@aotter/mantle 0.2.0-alpha.8 → 0.2.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/README.md +26 -3
- package/dist/auth/adapter.js +10 -34
- package/dist/auth/admin.js +2 -5
- package/dist/auth/caller.js +0 -27
- package/dist/auth/consent.js +100 -0
- package/dist/auth/hardening.js +0 -7
- package/dist/auth/identity.js +0 -11
- package/dist/auth/oauth.js +0 -32
- package/dist/auth/resource-seeds.js +0 -2
- package/dist/auth/route-http.js +33 -0
- package/dist/auth/routes.js +8 -153
- package/dist/build/compiler.d.ts +19 -609
- package/dist/build/compiler.js +3 -6896
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
- package/dist/bun/assets.js +0 -2
- package/dist/domain/adminOrder.js +0 -2
- package/dist/domain/columns.js +7 -20
- package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
- package/dist/domain/describe/index.js +1 -115
- package/dist/domain/describe/target-hash.js +1 -12
- package/dist/domain/entry.js +0 -14
- package/dist/domain/generated-tools.js +0 -1
- package/dist/domain/input.js +0 -2
- package/dist/domain/paging.js +0 -17
- package/dist/domain/recipes.js +10 -7
- package/dist/domain/userReferences.js +0 -1
- package/dist/domain/wireScalars.js +0 -6
- package/dist/infrastructure/adminEntries.js +10 -7
- package/dist/infrastructure/adminPaging.js +55 -57
- package/dist/infrastructure/artifacts.js +0 -4
- package/dist/infrastructure/authSchema.js +0 -9
- package/dist/infrastructure/bundle.js +17 -32
- package/dist/infrastructure/cli.js +25 -11
- package/dist/infrastructure/generate.js +18 -17
- package/dist/infrastructure/generateAuth.js +48 -5
- package/dist/infrastructure/help.js +55 -0
- package/dist/infrastructure/init.js +50 -0
- package/dist/infrastructure/inputs.js +0 -9
- package/dist/infrastructure/mounts.js +22 -0
- package/dist/infrastructure/wrangler.js +18 -9
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +16 -7
- package/dist/kernel/binds.js +4 -10
- package/dist/kernel/caller-identity.d.ts +0 -3
- package/dist/kernel/caller-identity.js +0 -15
- package/dist/kernel/caller-mode.d.ts +4 -0
- package/dist/kernel/caller-mode.js +14 -0
- package/dist/kernel/contracts.d.ts +7 -11
- package/dist/kernel/contracts.js +8 -44
- package/dist/kernel/cron.js +0 -4
- package/dist/kernel/dialect/common.d.ts +0 -1
- package/dist/kernel/dialect/common.js +0 -3
- package/dist/kernel/dialect/postgres.js +0 -2
- package/dist/kernel/dialect/sqlite.js +1 -6
- package/dist/kernel/drain.js +2 -8
- package/dist/kernel/driver/bun-sqlite.js +0 -4
- package/dist/kernel/driver/d1.js +0 -1
- package/dist/kernel/driver/postgres.js +3 -17
- package/dist/kernel/driver/transaction-context.js +0 -1
- package/dist/kernel/email.js +0 -4
- package/dist/kernel/host-access.js +2 -3
- package/dist/kernel/host-caller.d.ts +1 -1
- package/dist/kernel/host-caller.js +3 -6
- package/dist/kernel/json-body.js +0 -2
- package/dist/kernel/mcp-apps.js +1 -6
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +3 -27
- package/dist/kernel/mcp.d.ts +5 -1
- package/dist/kernel/mcp.js +2 -6
- package/dist/kernel/media.js +0 -18
- package/dist/kernel/paging.js +1 -9
- package/dist/kernel/publishing.js +3 -4
- package/dist/kernel/r2-media.js +0 -7
- package/dist/kernel/rest-dispatch.js +0 -1
- package/dist/kernel/rest-router.js +2 -3
- package/dist/kernel/rest.js +0 -3
- package/dist/kernel/scalars.js +4 -11
- package/dist/kernel/search.js +0 -1
- package/dist/kernel/site.js +0 -2
- package/dist/kernel/store.d.ts +11 -1
- package/dist/kernel/store.js +21 -34
- package/dist/kernel/surface-failure.d.ts +10 -0
- package/dist/kernel/surface-failure.js +29 -0
- package/dist/kernel/surface.js +0 -3
- package/dist/kernel/sweep.js +0 -1
- package/dist/kernel/translations.js +0 -1
- package/dist/kernel/user-caller.js +2 -8
- package/dist/kernel/user-fields.d.ts +3 -2
- package/dist/kernel/user-fields.js +9 -9
- package/dist/kernel/validation.js +0 -8
- package/dist/kernel/web-gateway.js +0 -2
- package/dist/kernel/web-head.js +0 -1
- package/dist/kernel/web-index.js +39 -0
- package/dist/kernel/web-response.js +0 -1
- package/dist/kernel/web-serve.js +97 -0
- package/dist/kernel/web.d.ts +1 -1
- package/dist/kernel/web.js +10 -125
- package/dist/kernel/where.js +0 -6
- package/dist/usecase/adminBindings.js +30 -21
- package/dist/usecase/adminClient.js +0 -21
- package/dist/usecase/adminDescriptors.js +2 -9
- package/dist/usecase/generate.js +7 -12
- package/dist/usecase/handlers.js +0 -3
- package/dist/usecase/mcp-media.js +0 -1
- package/dist/usecase/mcp.js +0 -4
- package/dist/usecase/paging.js +1 -3
- package/dist/usecase/publishing.js +1 -5
- package/dist/usecase/rest.js +0 -1
- package/dist/usecase/site.js +0 -2
- package/dist/usecase/stores.js +4 -14
- package/dist/usecase/tool-version.js +0 -3
- package/dist/usecase/translations.js +0 -2
- package/dist/usecase/validation.js +8 -20
- package/docs/examples/README.md +13 -5
- package/docs/examples/app-shell/README.md +43 -8
- package/docs/examples/app-shell/accept.mjs +91 -0
- package/docs/examples/app-shell/gitignore +7 -0
- package/docs/examples/app-shell/package.json +7 -6
- package/docs/examples/app-shell/smoke-kit.mjs +29 -15
- package/docs/examples/app-shell/src/auth.ts +3 -2
- package/docs/examples/app-shell/src/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +4 -3
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +22 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/club-events/src/handlers.ts +1 -1
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +36 -3
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/purchasing/src/handlers.ts +1 -1
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/authentication.md +21 -4
- package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
- package/docs/handbook/cloudflare/deploy-and-operate.md +24 -1
- package/docs/handbook/cloudflare/media-r2.md +2 -3
- package/docs/handbook/cloudflare/service-entry.md +17 -4
- package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +15 -10
- package/docs/handbook/guides/agent-workflow-acceptance.md +232 -0
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/mcp-apps.md +78 -1
- package/docs/handbook/guides/native-admin.md +29 -6
- package/docs/handbook/guides/native-execution.md +5 -4
- package/docs/handbook/guides/oauth.md +58 -0
- package/docs/handbook/guides/public-pages.md +44 -4
- package/docs/handbook/guides/sql-patterns.md +266 -0
- package/docs/handbook/guides/typed-queries.md +173 -0
- package/docs/handbook/navigation.json +16 -0
- package/docs/handbook/reference/admin-descriptors.md +4 -1
- package/docs/handbook/reference/authorization.md +1 -1
- package/docs/handbook/reference/diagnostics.md +6 -1
- package/docs/handbook/reference/procedure.md +5 -5
- package/docs/handbook/reference/schema.md +10 -4
- package/docs/handbook/reference/sql.md +64 -0
- package/docs/handbook/reference/surface.md +11 -4
- package/docs/handbook/reference/trigger.md +9 -0
- package/docs/handbook/reference/view.md +11 -3
- package/docs/handbook/releases/index.md +27 -6
- package/docs/handbook/start/agents.md +45 -0
- package/docs/handbook/start/project-and-cli.md +15 -1
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +44 -16
- package/docs/skills/integrate/SKILL.md +4 -9
- package/docs/skills/provision/SKILL.md +15 -12
- package/docs/skills/update/SKILL.md +21 -18
- package/package.json +24 -4
- package/skills/README.md +1 -1
- package/skills/mantle/SKILL.md +48 -107
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/docs/skills/develop/mcp-app.md +0 -39
- package/skills/.keep +0 -0
- package/skills/mantle/references/local-testing.md +0 -15
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -49
|
@@ -2,10 +2,13 @@ import type { Caller } from "@aotter/mantle/contracts";
|
|
|
2
2
|
import { signInPage } from "./sign-in.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* App-owned routes, tried before
|
|
5
|
+
* App-owned routes, tried before REST. Return undefined to fall through. Paths under /admin and /mcp never reach here, and
|
|
6
|
+
* neither do Better Auth's (/api/auth/*, /oauth/consent*, /.well-known/oauth-*). A root catch-all page (/:slug) must
|
|
7
|
+
* still return undefined for the paths of the app's own HTTP Triggers (REST), or it shadows them.
|
|
6
8
|
* GET /sign-in: member sign-in (src/sign-in.ts); a member page sends anonymous visitors there with signInRedirect.
|
|
7
|
-
* AGENT: add visitor pages
|
|
8
|
-
* (
|
|
9
|
+
* AGENT: add visitor pages and callbacks/webhooks here. Visitor pages carry everything in `mantle help public-pages`
|
|
10
|
+
* (canonical, Open Graph, JSON-LD, answer block, .md alternates, sitemap.xml, robots.txt, llms.txt; recipes example).
|
|
11
|
+
* A callback must verify the raw Request (signature, timestamp) before it reads the body as JSON or calls a generated Procedure.
|
|
9
12
|
*/
|
|
10
13
|
export async function appRoutes(request: Request, _env: Env, _context: { caller: () => Promise<Caller>; waitUntil: (task: Promise<unknown>) => void }): Promise<Response | undefined> {
|
|
11
14
|
if (new URL(request.url).pathname === "/sign-in" && request.method === "GET") return signInPage();
|
|
@@ -43,7 +43,8 @@ export async function fetchService(request: Request, env: Env, context: Executio
|
|
|
43
43
|
const ownAuth = await withAuthTasks(context.waitUntil.bind(context), () => authRoutes(auth!, { methods: [{ kind: "email-otp" }], resolver: caller })(request));
|
|
44
44
|
if (ownAuth) return respond(ownAuth);
|
|
45
45
|
const url = new URL(request.url);
|
|
46
|
-
|
|
46
|
+
// /admin and /mcp belong to Admin and MCP: a root catch-all page in src/routes.ts (/:slug) never shadows them.
|
|
47
|
+
const own = /^\/(?:admin|mcp)(?:\/|$)/.test(url.pathname) ? undefined : await appRoutes(request, env, { caller, waitUntil: context.waitUntil.bind(context) });
|
|
47
48
|
if (own) return respond(own);
|
|
48
49
|
let response: Response | undefined;
|
|
49
50
|
if (url.pathname === "/admin/api" || url.pathname.startsWith("/admin/api/")) {
|
|
@@ -56,8 +57,8 @@ export async function fetchService(request: Request, env: Env, context: Executio
|
|
|
56
57
|
}
|
|
57
58
|
response = await serveAdmin(request, env);
|
|
58
59
|
} else if (/^\/mcp(?:\/admin)?\/*$/.test(url.pathname)) {
|
|
59
|
-
// --- MCP (
|
|
60
|
-
serveMcpHandler ??= serveMcp(tools, { ...options, apps: { admin: adminMcpApps({ catalogue: mcpAppCatalogues.admin }) } });
|
|
60
|
+
// --- MCP / OAuth (deliberate exception): remove with the auth plugins only for intentional asymmetric exposure.
|
|
61
|
+
serveMcpHandler ??= serveMcp(tools, { ...options, authorizationScopes: ["offline_access"], apps: { admin: adminMcpApps({ catalogue: mcpAppCatalogues.admin }) } });
|
|
61
62
|
response = await serveMcpHandler(request, env);
|
|
62
63
|
} else {
|
|
63
64
|
response = await serveRest(request, env);
|
|
@@ -23,6 +23,9 @@ over MCP. Only members the officers listed in Admin can sign up. Officers see ev
|
|
|
23
23
|
Schema check; no View or Trigger exposes it). `src/signup.ts` replaces the shell's allow-all `allowSignUp` with a
|
|
24
24
|
Store lookup as a system caller, so any other email gets 403 at sign-in and no account. To invite another officer
|
|
25
25
|
through Admin, list their email in `members` first.
|
|
26
|
+
- **An event with its attendees in one read**: the admin View `event-roster` returns each event with an `attendees`
|
|
27
|
+
JSON array built by `json_group_array(json_set('{}', ...))` in a correlated subquery, so an event nobody joined has
|
|
28
|
+
`[]`. PostgreSQL writes it with `jsonb_agg(jsonb_build_object(...))`; see `mantle help sql-patterns`.
|
|
26
29
|
- **Member MCP tool**: the `register-mcp` Trigger (`surface: public`) exposes `register` on `/mcp`, so the member's AI
|
|
27
30
|
registers with the member's own sign-in.
|
|
28
31
|
|
|
@@ -36,9 +39,11 @@ over MCP. Only members the officers listed in Admin can sign up. Officers see ev
|
|
|
36
39
|
|
|
37
40
|
## Run it
|
|
38
41
|
|
|
42
|
+
This runs the example by itself, to read and try. For your own app, run `mantle init` and take only the patterns you need into its manifests and `src/` (`mantle help examples`).
|
|
43
|
+
|
|
39
44
|
```sh
|
|
40
|
-
|
|
41
|
-
cp -R node_modules/@aotter/mantle/docs/examples/club-events/. my-app/
|
|
45
|
+
npx mantle init my-app
|
|
46
|
+
cp -R node_modules/@aotter/mantle/docs/examples/club-events/. my-app/ # run the example as is
|
|
42
47
|
cd my-app && pnpm install && pnpm generate && pnpm types && pnpm check
|
|
43
48
|
cp .dev.vars.example .dev.vars && pnpm db:init && pnpm dev
|
|
44
49
|
pnpm smoke
|
|
@@ -86,6 +86,21 @@ spec:
|
|
|
86
86
|
FROM registrations r JOIN events e ON e.id = r.eventId
|
|
87
87
|
ORDER BY r.created_at DESC, r.id DESC
|
|
88
88
|
---
|
|
89
|
+
# Officers' roster: each event with its attendees as one JSON array, so one read answers "who is coming".
|
|
90
|
+
# The correlated subquery gives [] for an event nobody registered for (a LEFT JOIN + GROUP BY would give one empty object).
|
|
91
|
+
# SQLite/D1 builds the objects with json_set; on PostgreSQL write jsonb_agg(jsonb_build_object('memberName', r.memberName)).
|
|
92
|
+
apiVersion: cms.mantle.aotter.net/v2
|
|
93
|
+
kind: View
|
|
94
|
+
metadata: { name: event-roster }
|
|
95
|
+
spec:
|
|
96
|
+
title: Event roster
|
|
97
|
+
surface: admin
|
|
98
|
+
requires: { auth: { all: [ctx.admin] } }
|
|
99
|
+
sql: |
|
|
100
|
+
SELECT e.id, e.title, e.startsAt AS "startsAt", e.seats_taken AS "seatsTaken",
|
|
101
|
+
(SELECT json_group_array(json_set('{}', '$.memberName', r.memberName)) FROM registrations r WHERE r.eventId = e.id) AS attendees
|
|
102
|
+
FROM events e ORDER BY e.startsAt, e.id
|
|
103
|
+
---
|
|
89
104
|
# The member-facing entry: a ref handler checks the event, then runs register-write.
|
|
90
105
|
apiVersion: cms.mantle.aotter.net/v2
|
|
91
106
|
kind: Procedure
|
|
@@ -165,6 +180,13 @@ spec:
|
|
|
165
180
|
source: { kind: http, method: POST, path: /api/registrations/cancel }
|
|
166
181
|
target: { procedure: cancel-registration }
|
|
167
182
|
---
|
|
183
|
+
apiVersion: cms.mantle.aotter.net/v2
|
|
184
|
+
kind: Trigger
|
|
185
|
+
metadata: { name: cancel-registration-mcp }
|
|
186
|
+
spec:
|
|
187
|
+
source: { kind: mcp, surface: public }
|
|
188
|
+
target: { procedure: cancel-registration }
|
|
189
|
+
---
|
|
168
190
|
# Officers add records through declared Procedures: operational collections have no generic create in Admin.
|
|
169
191
|
# collectionAction puts the Procedure's button and input form in that collection's list header; the admin MCP
|
|
170
192
|
# Trigger is what makes it an Admin operation (and lets the officers' AI do the same).
|
|
@@ -105,7 +105,13 @@ await smoke(async ({ call, mcp, signIn, sql, owner, step, logs }) => {
|
|
|
105
105
|
assert.equal((await call("GET", "/admin/api/entries?collection=registrations", { cookie: bob })).status, 403);
|
|
106
106
|
assert.equal((await call("GET", "/admin/api/me", { cookie: bob })).status, 403);
|
|
107
107
|
assert.ok(all.body.items.every(r => r.data.memberName && r.data.memberName !== r.data.member), JSON.stringify(all.body.items));
|
|
108
|
-
|
|
108
|
+
const roster = await call("GET", "/admin/api/views/event-roster", { cookie: admin });
|
|
109
|
+
assert.equal(roster.status, 200, JSON.stringify(roster.body));
|
|
110
|
+
const attendees = row => (typeof row.attendees === "string" ? JSON.parse(row.attendees) : row.attendees).map(a => a.memberName).sort();
|
|
111
|
+
for (const row of roster.body.rows) assert.equal(attendees(row).length, row.seatsTaken, JSON.stringify(row));
|
|
112
|
+
assert.ok(roster.body.rows.some(row => attendees(row).length > 0), JSON.stringify(roster.body.rows));
|
|
113
|
+
assert.equal((await call("GET", "/admin/api/views/event-roster", { cookie: bob })).status, 403);
|
|
114
|
+
step("officers see every member's registrations, with names, through Admin's API; and every event with its attendees as one JSON array (event-roster); a member gets 403 on Admin");
|
|
109
115
|
|
|
110
116
|
const entry = async id => (await call("GET", `/admin/api/entries/${id}?collection=events`, { cookie: admin })).body.entry;
|
|
111
117
|
const before = await seatsOf(hike.id), read = await entry(hike.id);
|
|
@@ -17,7 +17,7 @@ export const handlers: Handlers = {
|
|
|
17
17
|
return await store.procedures["register-write"]({ eventId, member: caller.subject, memberName: user?.name || user?.email || caller.subject });
|
|
18
18
|
} catch (error) {
|
|
19
19
|
// register-write's only Schema check is the capacity one.
|
|
20
|
-
if (error instanceof ContractError && error.code === "CONFLICT" && error.
|
|
20
|
+
if (error instanceof ContractError && error.code === "CONFLICT" && error.fields?.kind === "check" && error.fields.schema === "events")
|
|
21
21
|
throw new ContractError("CONFLICT", "The event is full.", "procedures/register");
|
|
22
22
|
throw error;
|
|
23
23
|
}
|
|
@@ -14,6 +14,13 @@ that have been waiting too long.
|
|
|
14
14
|
`change-amount`, `mark-ordered`, `confirm-order`). The column is `requestStatus` because `status` is reserved for
|
|
15
15
|
publishing Schemas. `requests` and `ledger` set `readOnly: true`, so Admin's generic editor cannot bypass the state
|
|
16
16
|
machine (for example by moving an ordered request back to `submitted` and approving it again).
|
|
17
|
+
- **A status chosen at submission**: `submit-request` uses `CASE WHEN input.amount <= 100 THEN 'approved' ELSE
|
|
18
|
+
'submitted' END`. A small request also spends the budget and writes its ledger row in the same program, so
|
|
19
|
+
`remaining >= 0` refuses all three when it would overspend. Those two statements find the request by
|
|
20
|
+
`created_at = now()` (the instant of this program) and `NOT EXISTS` a ledger row, so the threshold is written only in
|
|
21
|
+
the `CASE`. Larger requests wait for an admin. An auto-approved request sends no approval email, and the daily cron
|
|
22
|
+
orders it from the supplier (it already retries every `approved` request); route small requests through a `ref`
|
|
23
|
+
handler like `approve-request` if they must be ordered and announced at once.
|
|
17
24
|
- **Admin-only operations**: `approve-request` and `reject-request` declare `requires: ctx.admin` and a `target`, so
|
|
18
25
|
Admin shows them as operations on a request. Their `kind: mcp, surface: admin` Triggers make them
|
|
19
26
|
`approve_request` / `reject_request` tools on `/mcp/admin`. They are not listed on `/mcp`.
|
|
@@ -56,11 +63,13 @@ scheme.
|
|
|
56
63
|
- Need another approval flow? Copy the pattern: a ref Procedure (admin-only, side effects) that wraps a guarded SQL
|
|
57
64
|
write Procedure (one transaction).
|
|
58
65
|
|
|
59
|
-
##
|
|
66
|
+
## Run it
|
|
67
|
+
|
|
68
|
+
This runs the example by itself, to read and try. For your own app, run `mantle init` and take only the patterns you need into its manifests and `src/` (`mantle help examples`).
|
|
60
69
|
|
|
61
70
|
```sh
|
|
62
|
-
|
|
63
|
-
cp -R node_modules/@aotter/mantle/docs/examples/purchasing/. my-app/
|
|
71
|
+
npx mantle init my-app
|
|
72
|
+
cp -R node_modules/@aotter/mantle/docs/examples/purchasing/. my-app/ # run the example as is
|
|
64
73
|
cd my-app && pnpm install && pnpm generate && pnpm types && pnpm check
|
|
65
74
|
cp .dev.vars.example .dev.vars # then set SUPPLIER_API_URL and SUPPLIER_WEBHOOK_SECRET
|
|
66
75
|
pnpm db:init && pnpm db:seed && pnpm dev
|
|
@@ -48,10 +48,12 @@ spec:
|
|
|
48
48
|
title: Budget ledger
|
|
49
49
|
lifecycle: operational
|
|
50
50
|
indexes: [[createdAt, id]]
|
|
51
|
+
# One row per spending request.
|
|
52
|
+
uniqueIndexes: [[requestId]]
|
|
51
53
|
uiSchema: { list: { primaryField: department, columns: [amount, remainingAfter, requestId] } }
|
|
52
54
|
schema:
|
|
53
55
|
type: object
|
|
54
|
-
readOnly: true # written only by approve-request-write
|
|
56
|
+
readOnly: true # written only by approve-request-write and submit-request
|
|
55
57
|
additionalProperties: false
|
|
56
58
|
required: [requestId, department, amount, remainingAfter]
|
|
57
59
|
properties:
|
|
@@ -117,6 +119,7 @@ kind: Procedure
|
|
|
117
119
|
metadata: { name: submit-request }
|
|
118
120
|
spec:
|
|
119
121
|
title: Submit a purchase request
|
|
122
|
+
description: Submit a purchase request against an existing department budget; a request of 100 or less is approved at once and spends the budget.
|
|
120
123
|
requires: { auth: { all: [ctx.user] } }
|
|
121
124
|
input:
|
|
122
125
|
type: object
|
|
@@ -127,11 +130,26 @@ spec:
|
|
|
127
130
|
amount: { type: integer, minimum: 1 }
|
|
128
131
|
department: { type: string }
|
|
129
132
|
output: { type: object }
|
|
133
|
+
# One program. CASE picks the status: up to 100 is approved now, more waits for an admin. The budget and ledger
|
|
134
|
+
# statements then act only on "the request this program approved": the caller's, created by this program
|
|
135
|
+
# (created_at = now(), the same instant Mantle writes into created_at), approved, and with no ledger row yet. So the
|
|
136
|
+
# threshold lives only in the CASE, and remaining >= 0 refuses all three statements when a small request would
|
|
137
|
+
# overspend. A pending request matches nothing there, which a set form (IN (SELECT ...), INSERT ... SELECT) allows.
|
|
130
138
|
handler:
|
|
131
139
|
sql: |
|
|
132
140
|
INSERT INTO requests (title, amount, department, requestStatus)
|
|
133
|
-
SELECT input.title, input.amount, b.department,
|
|
134
|
-
|
|
141
|
+
SELECT input.title, input.amount, b.department, CASE WHEN input.amount <= 100 THEN 'approved' ELSE 'submitted' END
|
|
142
|
+
FROM budgets b WHERE b.department = input.department
|
|
143
|
+
RETURNING id, version, requestStatus AS "requestStatus";
|
|
144
|
+
UPDATE budgets SET remaining = remaining - (SELECT SUM(r.amount) FROM requests r WHERE r.department = budgets.department
|
|
145
|
+
AND r.author_id = auth.uid() AND r.created_at = now() AND r.requestStatus = 'approved'
|
|
146
|
+
AND NOT EXISTS (SELECT 1 FROM ledger l WHERE l.requestId = r.id))
|
|
147
|
+
WHERE department IN (SELECT r.department FROM requests r WHERE r.author_id = auth.uid() AND r.created_at = now()
|
|
148
|
+
AND r.requestStatus = 'approved' AND NOT EXISTS (SELECT 1 FROM ledger l WHERE l.requestId = r.id));
|
|
149
|
+
INSERT INTO ledger (requestId, department, amount, remainingAfter)
|
|
150
|
+
SELECT r.id, r.department, r.amount, b.remaining FROM requests r JOIN budgets b ON b.department = r.department
|
|
151
|
+
WHERE r.author_id = auth.uid() AND r.created_at = now() AND r.requestStatus = 'approved'
|
|
152
|
+
AND NOT EXISTS (SELECT 1 FROM ledger l WHERE l.requestId = r.id)
|
|
135
153
|
---
|
|
136
154
|
# The requester may change the amount while the request is still 'submitted'. The version moves on, so an admin
|
|
137
155
|
# approving the version they reviewed before the change gets a 409 and looks again.
|
|
@@ -140,6 +158,7 @@ kind: Procedure
|
|
|
140
158
|
metadata: { name: change-amount }
|
|
141
159
|
spec:
|
|
142
160
|
title: Change the amount of my request
|
|
161
|
+
description: Change the amount of the signed-in member's submitted purchase request.
|
|
143
162
|
requires: { auth: { all: [ctx.user] } }
|
|
144
163
|
restrict: { requests: "author_id = auth.uid()" }
|
|
145
164
|
input:
|
|
@@ -291,11 +310,25 @@ spec:
|
|
|
291
310
|
---
|
|
292
311
|
apiVersion: cms.mantle.aotter.net/v2
|
|
293
312
|
kind: Trigger
|
|
313
|
+
metadata: { name: submit-request-mcp }
|
|
314
|
+
spec:
|
|
315
|
+
source: { kind: mcp, surface: public }
|
|
316
|
+
target: { procedure: submit-request }
|
|
317
|
+
---
|
|
318
|
+
apiVersion: cms.mantle.aotter.net/v2
|
|
319
|
+
kind: Trigger
|
|
294
320
|
metadata: { name: change-amount-http }
|
|
295
321
|
spec:
|
|
296
322
|
source: { kind: http, method: POST, path: '/api/requests/{id}/amount' }
|
|
297
323
|
target: { procedure: change-amount }
|
|
298
324
|
---
|
|
325
|
+
apiVersion: cms.mantle.aotter.net/v2
|
|
326
|
+
kind: Trigger
|
|
327
|
+
metadata: { name: change-amount-mcp }
|
|
328
|
+
spec:
|
|
329
|
+
source: { kind: mcp, surface: public }
|
|
330
|
+
target: { procedure: change-amount }
|
|
331
|
+
---
|
|
299
332
|
# Admin MCP tools: an AI agent connected to /mcp/admin can approve or reject.
|
|
300
333
|
apiVersion: cms.mantle.aotter.net/v2
|
|
301
334
|
kind: Trigger
|
|
@@ -171,6 +171,20 @@ try {
|
|
|
171
171
|
assert.deepEqual(received.map(order => order.reference), [laptop, projector]);
|
|
172
172
|
assert.deepEqual(await statusAndOrder(projector), { status: "ordered", supplierOrderId: "SUP-2" });
|
|
173
173
|
step("daily cron retries the failed supplier order (system caller) and emails every admin about requests pending more than 3 days, and only those");
|
|
174
|
+
|
|
175
|
+
const small = await call("POST", "/api/requests", { cookie: bob, body: { title: "Pens", amount: 50, department: "Engineering" } });
|
|
176
|
+
assert.equal(small.status, 200, JSON.stringify(small.body));
|
|
177
|
+
const pens = small.body.results[0][0];
|
|
178
|
+
assert.equal(pens.requestStatus, "approved");
|
|
179
|
+
const spent = await remaining("Engineering");
|
|
180
|
+
assert.deepEqual((await ledgerRows()).filter(r => r.requestId === pens.id), [{ requestId: pens.id, amount: 50, remainingAfter: spent }]);
|
|
181
|
+
await query(`UPDATE budgets SET remaining = 20 WHERE department = 'Design'`);
|
|
182
|
+
const ledgerBefore = (await ledgerRows()).length, requestsBefore = (await query("SELECT count(*) AS n FROM requests"))[0].n;
|
|
183
|
+
assert.equal((await call("POST", "/api/requests", { cookie: alice, body: { title: "Tape", amount: 30, department: "Design" } })).status, 409);
|
|
184
|
+
assert.equal(await remaining("Design"), 20);
|
|
185
|
+
assert.equal((await ledgerRows()).length, ledgerBefore);
|
|
186
|
+
assert.equal((await query("SELECT count(*) AS n FROM requests"))[0].n, requestsBefore);
|
|
187
|
+
step("CASE approves a request of 100 or less at submission: the status, the budget and the ledger row commit together, and one that would overspend is refused with nothing written");
|
|
174
188
|
}, { vars: { EMPLOYEE_DOMAIN: "example.test", SUPPLIER_API_URL: `http://127.0.0.1:${supplier.address().port}/orders`, SUPPLIER_WEBHOOK_SECRET: SECRET } });
|
|
175
189
|
} finally {
|
|
176
190
|
supplier.close();
|
|
@@ -14,7 +14,7 @@ export const handlers: Handlers = {
|
|
|
14
14
|
} catch (error) {
|
|
15
15
|
// The declared budget check is a business refusal, not a server error. Nothing was written.
|
|
16
16
|
// The approval's only Schema check is the budget one.
|
|
17
|
-
if (error instanceof ContractError && error.code === "CONFLICT" && error.
|
|
17
|
+
if (error instanceof ContractError && error.code === "CONFLICT" && error.fields?.kind === "check" && error.fields.schema === "budgets")
|
|
18
18
|
throw new ContractError("CONFLICT", "The department budget cannot cover this request.", "procedures/approve-request");
|
|
19
19
|
throw error;
|
|
20
20
|
}
|
|
@@ -25,11 +25,13 @@ Search for `AGENT:` for what an app must still provide: layout and styling, cach
|
|
|
25
25
|
- Need members to see or edit only their own rows? Add club-events' `restrict: { recipes: "author_id = auth.uid()" }` on a View.
|
|
26
26
|
- Need review before publishing, or an AI that approves? Add purchasing's guarded status transitions and admin MCP.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Run it
|
|
29
|
+
|
|
30
|
+
This runs the example by itself, to read and try. For your own app, run `mantle init` and take only the patterns you need into its manifests and `src/` (`mantle help examples`).
|
|
29
31
|
|
|
30
32
|
```sh
|
|
31
|
-
|
|
32
|
-
cp -R node_modules/@aotter/mantle/docs/examples/recipes/. my-app/
|
|
33
|
+
npx mantle init my-app
|
|
34
|
+
cp -R node_modules/@aotter/mantle/docs/examples/recipes/. my-app/ # run the example as is
|
|
33
35
|
cd my-app && pnpm install && pnpm generate && pnpm types && pnpm check
|
|
34
36
|
```
|
|
35
37
|
|
|
@@ -12,6 +12,8 @@ Mantle supplies the database adapter, schema generation and caller resolution:
|
|
|
12
12
|
|
|
13
13
|
## `auth.ts`
|
|
14
14
|
|
|
15
|
+
> **Verification status:** the GitHub social-provider and magic-link recipes have not yet been exercised end to end against real deployed provider flows.
|
|
16
|
+
|
|
15
17
|
Copy `src/auth.ts` from the [app shell](../../examples/app-shell/README.md), which CI runs from the packed packages;
|
|
16
18
|
installed, it is `node_modules/@aotter/mantle/docs/examples/app-shell/src/auth.ts`. Its essentials:
|
|
17
19
|
|
|
@@ -78,6 +80,7 @@ after success; invitations still work when email is absent.
|
|
|
78
80
|
`databaseHooks.user.create.after` (and user.update.after for a later email verification). Only the first matching signup wins (case-insensitive); email matching requires emailVerified=true (required for bootstrap).
|
|
79
81
|
D1 uses one conditional UPDATE; PostgreSQL uses a transaction advisory lock before UPDATE, avoiding concurrent winners.
|
|
80
82
|
A blocked promotion logs a warning. A GitHub rule requires a configured trusted GitHub provider/profile mapper.
|
|
83
|
+
The promotion runs after Better Auth creates the user, so the first sign-in response (and a cached session payload) can still show the previous role, while authorization re-reads the role on every request and `/admin/api/me` and later sessions show the admin role.
|
|
81
84
|
Helpers run no boot queries or DDL.
|
|
82
85
|
|
|
83
86
|
## Worker entry
|
|
@@ -184,10 +187,24 @@ The default is one instance with `admin` and `user` roles. An app that keeps two
|
|
|
184
187
|
staff and customers) can run one Better Auth instance per population in the same Worker; every Mantle primitive
|
|
185
188
|
takes the instance it works on:
|
|
186
189
|
|
|
187
|
-
- Give each instance its own `basePath`, `advanced.cookiePrefix`, `secret` and table names (`user.modelName`,
|
|
188
|
-
`session.modelName`,
|
|
189
|
-
one `--auth` per module to `mantle generate`; the tables are merged into one migration, and two
|
|
190
|
-
define the same table differently are refused. The native adapter follows each instance's mapping.
|
|
190
|
+
- Give each instance its own `basePath`, `advanced.cookiePrefix`, `secret` and core table names (`user.modelName`,
|
|
191
|
+
`session.modelName`, `account.modelName`, `verification.modelName`). Keep each instance's options in its own
|
|
192
|
+
module and pass one `--auth` per module to `mantle generate`; the tables are merged into one migration, and two
|
|
193
|
+
instances that define the same table differently are refused. The native adapter follows each instance's mapping.
|
|
194
|
+
- Better Auth plugin factories share their schema object, so differing plugin field/model mappings across instances
|
|
195
|
+
in one Worker override each other. Use one of two supported ways:
|
|
196
|
+
1. Separate the instances by core table names only and leave every plugin mapping at its default.
|
|
197
|
+
2. When plugin tables must differ (`jwt`'s `jwks`, `@better-auth/oauth-provider`'s `oauth*` tables), map them
|
|
198
|
+
explicitly in every instance, the default one included, and give each instance its own copy of the plugin's
|
|
199
|
+
schema right after constructing it:
|
|
200
|
+
`const keys = jwt({ schema: { jwks: { modelName: "staff_jwks" } } }); const plugin = { ...keys, schema: structuredClone(keys.schema) };`
|
|
201
|
+
|
|
202
|
+
`mantle generate` refuses anything else when two or more `--auth` instances use the same plugin (`jwt`, `admin`,
|
|
203
|
+
`oauth-provider`, `two-factor`, `phone-number`, `anonymous`, `siwe`, `device-authorization`) and a mapping exists
|
|
204
|
+
while an instance keeps the shared schema object, or two instances resolve a mapping to the same name (generate
|
|
205
|
+
cannot tell an identical mapping from an inherited one, so mapped names must differ between instances). Build
|
|
206
|
+
every instance's plugins in the same order in the Worker as in `--auth`; an instance built earlier than a mapped
|
|
207
|
+
one can still inherit its mapping, which generate cannot see.
|
|
191
208
|
- One `authLoader(load, cookiePrefix)` per instance; call `resolveCaller(request, loader)` with the instance the
|
|
192
209
|
route belongs to, and mount `authRoutes(loader, ...)` per instance (it guards that instance's `/admin/*`). The
|
|
193
210
|
loader takes `cookiePrefix` separately on purpose: it checks the cookie before loading Better Auth, so anonymous
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Mantle on ChatGPT Sites
|
|
2
2
|
|
|
3
|
+
> **Verification status:** this guide is experimental and has not yet been exercised end to end on a real ChatGPT Site.
|
|
4
|
+
|
|
3
5
|
Treat the Site as an existing TypeScript host. Import selected generated Views/Procedures, pass the host's native handle and verified Caller, and compose only the surfaces it supports. Use [partial integration](../../skills/integrate/SKILL.md); Mantle supplies no Sites-specific identity or deployment adapter.
|
|
4
6
|
|
|
5
7
|
Choose postgres or sqlite according to the host's actual SQL executor. Custom handles are app-owned; a structural match alone does not guarantee driver semantics. [Database boundaries](../concepts/runtime-and-adapters.md) defines the supported drivers.
|
|
6
8
|
|
|
7
|
-
The runtime never creates, alters or checks tables. Apply initial `schema.sql` and selected auth/site DDL only to an empty database. For later changes the agent diffs regenerated SQL, writes and rehearses a host-native migration on a copy, and applies it before publishing new code.
|
|
9
|
+
The runtime never creates, alters or checks tables. Apply initial `schema.sql` and selected auth/site DDL only to an empty database. For later changes the agent diffs regenerated SQL, writes and rehearses a host-native migration on a copy, and applies it before publishing new code. Destructive/type changes require explicit data migration; Mantle drops nothing.
|
|
8
10
|
|
|
9
|
-
Use the Site's native publication workflow. Configure trusted origin and verified identity there; never construct a system/admin Caller from browser input. Serving Admin also requires its
|
|
11
|
+
Use the Site's native publication workflow. Configure trusted origin and verified identity there; never construct a system/admin Caller from browser input. Serving Admin also requires its app-built assets and shell security contract. Scheduling and storage provisioning belong to the host.
|
|
@@ -32,7 +32,7 @@ The runtime never creates, alters or checks tables. Boot and requests run no DDL
|
|
|
32
32
|
|
|
33
33
|
For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
|
|
34
34
|
|
|
35
|
-
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
35
|
+
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
36
36
|
|
|
37
37
|
Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
|
|
38
38
|
|
|
@@ -40,6 +40,29 @@ For D1, create a migration with `pnpm exec wrangler d1 migrations create <databa
|
|
|
40
40
|
|
|
41
41
|
For PostgreSQL, use the app's migration tool and deployment credentials. Plan lock waits and long index builds there; request credentials need no DDL privileges. Configure session settings on the role/database as described in [native execution](../guides/native-execution.md#postgresql-settings). Hyperdrive query caching must be disabled for authority and mutation visibility.
|
|
42
42
|
|
|
43
|
+
## Latency placement and D1 read replication
|
|
44
|
+
|
|
45
|
+
Measure these options against the application's real traffic; neither is a
|
|
46
|
+
default latency guarantee. [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/)
|
|
47
|
+
can move fetch execution closer to observed back-end services:
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
"placement": { "mode": "smart" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For globally distributed, read-heavy D1 traffic, enable
|
|
54
|
+
[read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/)
|
|
55
|
+
on the database and pass one request-scoped `env.DB.withSession(...)` handle to
|
|
56
|
+
the selected generated Views, Procedures and Store. Those D1 entry points accept
|
|
57
|
+
`D1DatabaseSession`. Without the Sessions API, queries continue to use the primary.
|
|
58
|
+
|
|
59
|
+
Use `first-unconstrained` only when a replica snapshot is acceptable,
|
|
60
|
+
`first-primary` when the flow must start from the latest primary state, or carry
|
|
61
|
+
a bookmark between logical sessions for sequential consistency. Reuse the same
|
|
62
|
+
session within that logical flow; all writes still go to the primary. Keep fresh
|
|
63
|
+
authentication/authorization checks and mutation/read-after-write paths on their
|
|
64
|
+
required current-authority path rather than weakening them for replica latency.
|
|
65
|
+
|
|
43
66
|
## Operate
|
|
44
67
|
|
|
45
68
|
Enable Workers Logs and traces in production. The top-level `observability.enabled` alone does not enable traces:
|
|
@@ -20,9 +20,8 @@ Each media purpose in `siteDefaults.media.purposes` lists mime **slots** and a b
|
|
|
20
20
|
`required` entries use `<input accept>` grammar (full mime, comma list, or a subtype shorthand such as `webp`);
|
|
21
21
|
`maxBytes` must cover every mime the slots allow, keyed by the full mime type.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
or credential management. Import `r2MediaStorage` from `@aotter/mantle/media/r2`:
|
|
23
|
+
Applications own their R2 binding, public bucket URL and S3 credentials; the SDK
|
|
24
|
+
package does not manage them. Import `r2MediaStorage` from `@aotter/mantle/media/r2`:
|
|
26
25
|
|
|
27
26
|
```ts
|
|
28
27
|
const storage = r2MediaStorage({
|
|
@@ -81,6 +81,9 @@ export default {
|
|
|
81
81
|
if (shell) return shell;
|
|
82
82
|
// Your public frontend/web.ts routes can use an anonymous Caller without
|
|
83
83
|
// loading auth; their generated SQL still controls publication visibility.
|
|
84
|
+
// Mounted here, after every Mantle surface, a root catch-all page (/:slug)
|
|
85
|
+
// cannot shadow /admin, /mcp or REST. Mount them earlier only behind a
|
|
86
|
+
// guard that skips those paths, as the app shell does.
|
|
84
87
|
return new Response('Not found', { status: 404, headers: { 'cache-control': 'no-store' } });
|
|
85
88
|
} catch (error) { return surfaceFailure(error); }
|
|
86
89
|
});
|
|
@@ -88,8 +91,11 @@ export default {
|
|
|
88
91
|
};
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
Keep the MCP and REST entries together for the normal symmetric path. For a deliberate
|
|
95
|
+
asymmetric exception, remove the omitted surface's imports and array entry. `mcp.ts`
|
|
96
|
+
exists only when the manifest has an MCP surface and requires the documented optional
|
|
97
|
+
MCP peer. An application that deliberately does not mount MCP need not import generated
|
|
98
|
+
`mcp.ts`; outside the entry import graph, it contributes nothing to the bundle.
|
|
93
99
|
Pass the same handlers to each selected surface that exposes ref Procedures.
|
|
94
100
|
Bind site/media/identity capabilities through the selected Admin/MCP
|
|
95
101
|
options; do not expose native auth administrative routes without `authRoutes` or
|
|
@@ -106,12 +112,19 @@ its collection parameter selects only a generated table and visibility policy.
|
|
|
106
112
|
|
|
107
113
|
## Workers assets and secrets
|
|
108
114
|
|
|
115
|
+
`mantle generate --admin --wrangler wrangler.jsonc` sets the selected assets
|
|
116
|
+
directory. A new assets block defaults to `binding: "ASSETS"`, `run_worker_first: true`
|
|
117
|
+
and `html_handling: "none"`. Existing blocks keep their binding and routing; when
|
|
118
|
+
`run_worker_first` is already true, a missing `html_handling` defaults to `none`.
|
|
119
|
+
A different binding needs a custom Admin HTML loader. Deploy the built assets and
|
|
120
|
+
Worker together.
|
|
121
|
+
|
|
109
122
|
`mantle generate cloudflare-d1 generated manifests/*.yaml --auth src/auth.ts --admin`
|
|
110
123
|
updates the split-build Wrangler keys (`main` and `base_dir` under `dist/worker`),
|
|
111
124
|
ensures `nodejs_compat` and defaults to `global_fetch_strictly_public` ([public fetch routing](deploy-and-operate.md#public-fetch-routing))
|
|
112
125
|
and, with `--admin`, `assets.directory`: `mantle bundle` builds Admin into
|
|
113
|
-
`dist/client/admin`, served at `/admin`. Configure
|
|
114
|
-
comments and unrelated settings survive regeneration:
|
|
126
|
+
`dist/client/admin`, served at `/admin`. Configure existing assets blocks explicitly;
|
|
127
|
+
JSONC comments and unrelated settings survive regeneration:
|
|
115
128
|
|
|
116
129
|
```jsonc
|
|
117
130
|
{
|
|
@@ -11,7 +11,7 @@ The runtime never creates, alters or checks tables. Boot and requests run no DDL
|
|
|
11
11
|
|
|
12
12
|
For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
|
|
13
13
|
|
|
14
|
-
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
14
|
+
For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
|
|
15
15
|
|
|
16
16
|
Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
|
|
17
17
|
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
Use the [Worker quickstart](../start/quickstart-worker.md) for a complete native manifest and entry, [typed queries](../guides/typed-queries.md) for embedding, and [service composition](../cloudflare/service-entry.md) for optional surfaces.
|
|
4
4
|
|
|
5
|
-
The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.0
|
|
5
|
+
The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.0/docs/examples/reference-service) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.
|
|
@@ -13,21 +13,20 @@ carries.
|
|
|
13
13
|
The Mantle plugin carries one OSS authoring skill, `mantle`, and local
|
|
14
14
|
acceptance references. It does not configure a hosted-service connection.
|
|
15
15
|
Install from the Git tag matching the exact SDK version you selected (currently
|
|
16
|
-
`v0.2.0
|
|
17
|
-
|
|
18
|
-
instructions. A skill install does not upgrade the application's SDK.
|
|
16
|
+
`v0.2.0`). Pin the tag to keep instructions aligned with the application's
|
|
17
|
+
SDK. A skill install does not upgrade the application's SDK.
|
|
19
18
|
|
|
20
19
|
For Codex, pin the marketplace checkout:
|
|
21
20
|
|
|
22
21
|
```sh
|
|
23
|
-
codex plugin marketplace add aotter/mantle --ref v0.2.0
|
|
22
|
+
codex plugin marketplace add aotter/mantle --ref v0.2.0
|
|
24
23
|
codex plugin add mantle@mantle
|
|
25
24
|
```
|
|
26
25
|
|
|
27
26
|
For Claude Code, first clone the release into a directory you will keep:
|
|
28
27
|
|
|
29
28
|
```sh
|
|
30
|
-
git clone --depth 1 --branch v0.2.0
|
|
29
|
+
git clone --depth 1 --branch v0.2.0 https://github.com/aotter/mantle.git mantle-plugin
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
Then, in Claude Code, use that checkout's absolute path:
|
|
@@ -41,7 +40,7 @@ Cursor and GitHub Copilot use their plugin manifests from the same tagged
|
|
|
41
40
|
checkout. Without a plugin host, install only the consumer skill by name:
|
|
42
41
|
|
|
43
42
|
```sh
|
|
44
|
-
npx skills add https://github.com/aotter/mantle/tree/v0.2.0
|
|
43
|
+
npx skills add https://github.com/aotter/mantle/tree/v0.2.0 --skill mantle
|
|
45
44
|
```
|
|
46
45
|
|
|
47
46
|
When upgrading the SDK, update the marketplace ref or local tagged checkout and
|
|
@@ -54,10 +53,16 @@ project and copies no handbook.
|
|
|
54
53
|
|
|
55
54
|
## 2. Install the SDK, then read its own docs
|
|
56
55
|
|
|
57
|
-
Use the version the project already pins. For a new project,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
Use the version the project already pins. For a new project, install stable
|
|
57
|
+
0.2.0 exactly:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pnpm add --save-exact @aotter/mantle@0.2.0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Add optional `@aotter/mantle-ui@0.2.0` and `@aotter/mantle-admin@0.2.0` only
|
|
64
|
+
when the selected surfaces need them. Every selected `@aotter/mantle*` package
|
|
65
|
+
stays at the same exact version. Read, from the application:
|
|
61
66
|
|
|
62
67
|
```text
|
|
63
68
|
node_modules/@aotter/mantle/README.md
|