@mortar-ai/skill 0.1.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 +61 -0
- package/bin/install.js +45 -0
- package/index.js +24 -0
- package/mortar-skill.md +250 -0
- package/package.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @mortar-ai/skill
|
|
2
|
+
|
|
3
|
+
[Claude Code](https://claude.ai/code) skill package for building on
|
|
4
|
+
**Mortar** (the BaaS that AI bundles talk to). Drop it into your skills
|
|
5
|
+
directory + Claude becomes instantly Mortar-fluent: knows the SDK
|
|
6
|
+
accessors, CLI commands, MCP tools, and — most importantly — the
|
|
7
|
+
tenant-isolation + `auth.tenant_id()` RLS rules that are the easiest
|
|
8
|
+
thing to get wrong.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -g @mortar-ai/skill
|
|
14
|
+
mortar-skill-install
|
|
15
|
+
# → installs to ~/.claude/skills/mortar/SKILL.md
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then restart Claude Code (or `/skill reload`).
|
|
19
|
+
|
|
20
|
+
Pairs naturally with the [Mortar MCP server](../mcp) — the skill teaches Claude
|
|
21
|
+
*how* to think about Mortar; the MCP gives it live tools to *act* on a
|
|
22
|
+
project. Install both for the full Supabase-plugin-equivalent
|
|
23
|
+
experience.
|
|
24
|
+
|
|
25
|
+
## What's inside
|
|
26
|
+
|
|
27
|
+
The skill teaches Claude:
|
|
28
|
+
|
|
29
|
+
- **SDK surface** — the `mortar.from()` / `auth` / `storage` /
|
|
30
|
+
`realtime` / `compute` / `usage` / `cron` / `queue` / `comms`
|
|
31
|
+
accessors + the account-scope `AccountClient`; which one to reach for
|
|
32
|
+
- **CLI commands** — `mortar login / use / db / storage / compute /
|
|
33
|
+
cron / queue / domains / comms / keys / tokens / topup / usage`
|
|
34
|
+
- **MCP tools** — the 40 `mortar_*` tools and when an agent should use
|
|
35
|
+
them vs the SDK
|
|
36
|
+
- **Tenant isolation + RLS** — `auth.tenant_id()` (Mortar's analog of
|
|
37
|
+
Supabase's `auth.uid()`), the `tenant_id = auth.tenant_id()` policy
|
|
38
|
+
shape, and why every per-project table carries a `tenant_id`
|
|
39
|
+
- **Architecture** — the 9 primitive × (5 core + 3 extension) feature
|
|
40
|
+
model, and why the SDK shape ≠ the billing shape
|
|
41
|
+
- **Common patterns** — auth (email+password / phone-OTP), data CRUD,
|
|
42
|
+
file upload + signed URLs, realtime subscribe, queue + cron
|
|
43
|
+
- **Anti-patterns** — assuming Fabric runs on Mortar; using `namespace`
|
|
44
|
+
instead of `tenant_id`; hand-rolling RLS the SDK already enforces
|
|
45
|
+
|
|
46
|
+
## Custom install location
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
mortar-skill-install --dest=/path/to/your/skills/dir/mortar.md
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Useful for team-shared skill repositories or in-repo skill packs.
|
|
53
|
+
|
|
54
|
+
## Updating
|
|
55
|
+
|
|
56
|
+
Re-run `mortar-skill-install` after `npm update @mortar-ai/skill`.
|
|
57
|
+
Idempotent — overwrites in place.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
Apache-2.0 (same as the rest of Mortar).
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mortar-skill-install — copies mortar-skill.md into the user's Claude
|
|
4
|
+
* Code skills directory.
|
|
5
|
+
*
|
|
6
|
+
* Default destination: `~/.claude/skills/mortar/SKILL.md`. Override
|
|
7
|
+
* via `--dest=<path>` for non-default Claude installs.
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: re-running overwrites the existing file in place.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { copyFileSync, existsSync, mkdirSync } = require('node:fs');
|
|
14
|
+
const { homedir } = require('node:os');
|
|
15
|
+
const { dirname, resolve } = require('node:path');
|
|
16
|
+
|
|
17
|
+
function parseFlags(argv) {
|
|
18
|
+
const out = { dest: undefined };
|
|
19
|
+
for (let i = 2; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a.startsWith('--dest=')) out.dest = a.slice('--dest='.length);
|
|
22
|
+
else if (a === '--dest') out.dest = argv[++i];
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
const flags = parseFlags(process.argv);
|
|
29
|
+
const src = resolve(__dirname, '..', 'mortar-skill.md');
|
|
30
|
+
if (!existsSync(src)) {
|
|
31
|
+
console.error(`mortar-skill: source missing at ${src}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const dest = flags.dest
|
|
35
|
+
? resolve(flags.dest)
|
|
36
|
+
: resolve(homedir(), '.claude', 'skills', 'mortar', 'SKILL.md');
|
|
37
|
+
|
|
38
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
39
|
+
copyFileSync(src, dest);
|
|
40
|
+
console.log(`✓ Installed Mortar skill → ${dest}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('Restart Claude Code (or run `/skill reload`) to activate.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
main();
|
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @mortar-ai/skill — exports the skill markdown for programmatic use.
|
|
2
|
+
//
|
|
3
|
+
// Consumers (e.g. a future `mortar install --self-update` flow) can
|
|
4
|
+
// require this package + push the markdown content into wherever they
|
|
5
|
+
// want; the bin/install.js script is the standard CLI path.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { readFileSync } = require('node:fs');
|
|
10
|
+
const { resolve } = require('node:path');
|
|
11
|
+
|
|
12
|
+
const SKILL_PATH = resolve(__dirname, 'mortar-skill.md');
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
/**
|
|
16
|
+
* skillContents() returns the raw markdown. Useful when a
|
|
17
|
+
* higher-level CLI wants to embed it into another package's skill
|
|
18
|
+
* output (symmetric with @keel-ai/skill's skillContents()).
|
|
19
|
+
*/
|
|
20
|
+
skillContents() {
|
|
21
|
+
return readFileSync(SKILL_PATH, 'utf8');
|
|
22
|
+
},
|
|
23
|
+
SKILL_PATH,
|
|
24
|
+
};
|
package/mortar-skill.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Mortar
|
|
3
|
+
description: Required guidance for Fabric apps that need a database, backend, authentication, persistence, file storage, realtime collaboration, cloud functions, or product analytics.
|
|
4
|
+
activation: auto
|
|
5
|
+
priority: high
|
|
6
|
+
triggers:
|
|
7
|
+
- keywords: [database, 数据库, backend, 后端, auth, authentication, 登录, 认证, persistence, 持久化, storage, upload, 文件, realtime, 实时, collaboration, compute, cloud, 云端]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Mortar for Fabric applications
|
|
11
|
+
|
|
12
|
+
Mortar is an independent Backend-as-a-Service used by Fabric-built apps; Fabric
|
|
13
|
+
itself does not run on Mortar. Do not infer its setup from another BaaS.
|
|
14
|
+
|
|
15
|
+
## Fabric integration contract
|
|
16
|
+
|
|
17
|
+
- Bundler projects (Web, Desktop renderer, Keel/React Native) import the
|
|
18
|
+
platform singleton only:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { mortar } from '@/integrations/mortar/client';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- WeChat has no `@/` alias. A page normally imports:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { mortar } from '../../utils/mortar/client';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `src/integrations/mortar/client.ts`, its `types.ts`, and the WeChat equivalents
|
|
31
|
+
under `utils/mortar/` are platform-owned. Never create, overwrite, or replace
|
|
32
|
+
them.
|
|
33
|
+
- Never add `VITE_MORTAR_*`, `MORTAR_URL`, or client-side API-key environment
|
|
34
|
+
variables. Never hand-write `createClient(...)`, `src/lib/mortar.ts`, or a
|
|
35
|
+
second wrapper/client in a Fabric project.
|
|
36
|
+
- Fabric seeds an import-safe singleton, then injects the tenant URL and PUBLIC
|
|
37
|
+
bundle key after provisioning. The public key belongs in app code; it is not
|
|
38
|
+
a server/admin secret.
|
|
39
|
+
- Add `@mortar-ai/client` to `dependencies` with npm when the app first needs
|
|
40
|
+
Mortar. The agent owns `package.json`, `package-lock.json`, and `node_modules`.
|
|
41
|
+
Run the install before starting the dev server.
|
|
42
|
+
- WeChat's injected singleton uses `@mortar-ai/client/wechat`, which supplies
|
|
43
|
+
`wx.request`, `wx.*StorageSync`, and chunked-request realtime adapters. Do not
|
|
44
|
+
replace it with raw `wx.request` calls to Mortar.
|
|
45
|
+
|
|
46
|
+
If the singleton reports that the backend is not connected, do not invent a
|
|
47
|
+
fallback URL or ask the user for keys. Use the platform schema/provisioning flow
|
|
48
|
+
below; if platform provisioning fails, report that platform failure.
|
|
49
|
+
|
|
50
|
+
## Required workflow
|
|
51
|
+
|
|
52
|
+
1. Call `mortar_schema` before designing or changing backend data. It returns
|
|
53
|
+
the current live schema, including console changes.
|
|
54
|
+
2. For each schema change, call `mortar_migrate` once with one operation. It
|
|
55
|
+
applies the live change first; only after success does Fabric append a
|
|
56
|
+
migration record under `mortar/migrations/`.
|
|
57
|
+
3. `mortar/migrations/` is a platform-owned output journal. Never create or edit
|
|
58
|
+
SQL there: a file in that directory does not execute or change the live
|
|
59
|
+
tenant. Do not edit or create `mortar.config.json`. Do not edit generated
|
|
60
|
+
`integrations/mortar/types.ts`; Fabric regenerates it from the live schema.
|
|
61
|
+
4. Run `npm install @mortar-ai/client` if the dependency is absent, then write app
|
|
62
|
+
code using the injected singleton.
|
|
63
|
+
5. Type-check and build. Handle every `{ data, error }` result in UI code.
|
|
64
|
+
|
|
65
|
+
Example schema operations (the tool schema is authoritative for all fields):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{"kind":"create_table","table":"todos","access":"private","columns":[{"name":"title","type":"text","required":true},{"name":"done","type":"bool"},{"name":"user_id","type":"uuid","required":true}]}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Supported migration kinds are `create_table`, `drop_table`, `set_table_access`,
|
|
72
|
+
`add_column`, `drop_column`, `create_index`, `set_bucket_access`, `deploy_function`, `delete_function`,
|
|
73
|
+
`upsert_cron`, and `delete_cron`. Destructive and backend-changing operations
|
|
74
|
+
must respect the workspace approval gate. This list is exhaustive: arbitrary
|
|
75
|
+
PostgreSQL, data backfills, PL/pgSQL functions, transactions, and triggers are
|
|
76
|
+
not migration inputs. If required behavior cannot be expressed by one of these
|
|
77
|
+
operations, report the unsupported Mortar capability instead of authoring a SQL
|
|
78
|
+
file that only looks executable.
|
|
79
|
+
|
|
80
|
+
To change an existing table's access policy, use `set_table_access`. Access is
|
|
81
|
+
metadata and must be updated in place. Never use `drop_table` followed by
|
|
82
|
+
`create_table` for a permission change: dropping a table deletes all of its
|
|
83
|
+
rows and columns.
|
|
84
|
+
|
|
85
|
+
For a public form, event registration, evidence upload, or similar operator-
|
|
86
|
+
reviewed intake, use `access: "public_insert"` on the destination table and
|
|
87
|
+
`set_bucket_access` with `access: "public_insert"` on any upload bucket. This
|
|
88
|
+
allows a public bundle client to create a new row/object while denying public
|
|
89
|
+
list, read, update, delete, and overwrite. Never use `public` merely to make an
|
|
90
|
+
anonymous form submit successfully; `public` exposes all rows/files for public
|
|
91
|
+
read and mutation. Generate an unguessable object key for every upload.
|
|
92
|
+
|
|
93
|
+
## Database SDK
|
|
94
|
+
|
|
95
|
+
The query surface is Supabase-shaped. Async row/auth/storage methods normally
|
|
96
|
+
return `{ data, error }`; check `error` rather than assuming success.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const { data: todos, error } = await mortar
|
|
100
|
+
.from('todos')
|
|
101
|
+
.select('*')
|
|
102
|
+
.eq('done', false)
|
|
103
|
+
.order('created_at', { ascending: false })
|
|
104
|
+
.limit(20);
|
|
105
|
+
if (error) throw new Error(error.message);
|
|
106
|
+
|
|
107
|
+
const { data: created, error: insertError } = await mortar
|
|
108
|
+
.from('todos')
|
|
109
|
+
.insert({ title: 'Ship', done: false, user_id: userId })
|
|
110
|
+
.select()
|
|
111
|
+
.single();
|
|
112
|
+
|
|
113
|
+
const { data: updated, error: updateError } = await mortar
|
|
114
|
+
.from('todos')
|
|
115
|
+
.update({ done: true })
|
|
116
|
+
.eq('id', todoId)
|
|
117
|
+
.select();
|
|
118
|
+
|
|
119
|
+
const { error: deleteError } = await mortar
|
|
120
|
+
.from('todos')
|
|
121
|
+
.delete()
|
|
122
|
+
.eq('id', todoId);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Useful chaining includes `eq`, `neq`, `gt/gte`, `lt/lte`, `like/ilike`, `in`,
|
|
126
|
+
`is`, `order`, `range`, `limit`, `single`, and `maybeSingle`.
|
|
127
|
+
|
|
128
|
+
## App-user authentication
|
|
129
|
+
|
|
130
|
+
Auth sessions are persisted and attached to later database, storage, realtime,
|
|
131
|
+
and compute calls by the SDK.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const { data, error } = await mortar.auth.signUp({ email, password });
|
|
135
|
+
const signedIn = await mortar.auth.signInWithPassword({ email, password });
|
|
136
|
+
|
|
137
|
+
const sent = await mortar.auth.signInWithOtp({ email }); // or { phone }
|
|
138
|
+
const verified = await mortar.auth.verifyOtp({ email, token });
|
|
139
|
+
|
|
140
|
+
const { data: sessionData } = await mortar.auth.getSession();
|
|
141
|
+
const { data: userData, error: userError } = await mortar.auth.getUser();
|
|
142
|
+
|
|
143
|
+
const { data: listener } = mortar.auth.onAuthStateChange((event, session) => {
|
|
144
|
+
// INITIAL_SESSION | SIGNED_IN | SIGNED_OUT | TOKEN_REFRESHED
|
|
145
|
+
});
|
|
146
|
+
listener.subscription.unsubscribe();
|
|
147
|
+
|
|
148
|
+
await mortar.auth.signOut();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Password reset starts with `mortar.auth.resetPasswordForEmail(email)`; confirm via
|
|
152
|
+
`passwordResetConfirm(email, token, newPassword)`. Never store or pass session
|
|
153
|
+
tokens manually unless the user explicitly needs a server integration.
|
|
154
|
+
|
|
155
|
+
For private user rows, include the authenticated user's ID in the schema/data
|
|
156
|
+
model and choose the correct table access policy. Client-side filters improve
|
|
157
|
+
UX but do not replace server-side access rules.
|
|
158
|
+
|
|
159
|
+
## File storage
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const bucket = mortar.storage.from('avatars');
|
|
163
|
+
|
|
164
|
+
const { data: uploaded, error: uploadError } = await bucket.upload(path, file, {
|
|
165
|
+
contentType: file.type,
|
|
166
|
+
upsert: true,
|
|
167
|
+
});
|
|
168
|
+
const { data: blob, error: downloadError } = await bucket.download(path);
|
|
169
|
+
const { data: files, error: listError } = await bucket.list('users/');
|
|
170
|
+
const { data: signed, error: signError } = await bucket.createSignedUrl(path, 3600);
|
|
171
|
+
const { error: removeError } = await bucket.remove([path]);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Downloads mint a short-lived URL and fetch the object directly from the storage
|
|
175
|
+
backend. Reuse `signed.signedUrl` until it nears expiry when rendering media.
|
|
176
|
+
Prefer the signed URL directly in `<img src>`, `<video src>`, or `<audio src>`;
|
|
177
|
+
this avoids an unnecessary blob copy and does not require CORS merely to render
|
|
178
|
+
the media. Signed-URL minting is excluded from the project's API-concurrency
|
|
179
|
+
budget, but clients should still lazy-load visible media and cache/reuse each
|
|
180
|
+
URL instead of signing again on every render. Signing authorizes a possible
|
|
181
|
+
download; it does not prove bytes were served and must never be described or
|
|
182
|
+
modeled as an object-egress charge. Only provider-observed access-log or billing
|
|
183
|
+
reconciliation may record actual object egress. Use the third
|
|
184
|
+
`createSignedUrl` argument to bound only the signing request when the UI needs
|
|
185
|
+
a deadline:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const { data: signed, error } = await bucket.createSignedUrl(path, 900, {
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
timeoutMs: 10_000,
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`bucket.download(path)` uses JavaScript `fetch()` against the object store and
|
|
195
|
+
therefore needs a matching bucket CORS rule in browsers. Appunvs-managed Mortar
|
|
196
|
+
storage provides that contract; self-hosted object storage must allow the app
|
|
197
|
+
origin for credential-free `GET` / `HEAD`. Never attach Mortar Authorization or
|
|
198
|
+
browser cookies to a signed object URL.
|
|
199
|
+
|
|
200
|
+
## Realtime, broadcast, and presence
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const room = mortar
|
|
204
|
+
.channel('todos-room', { presence: { key: userId } })
|
|
205
|
+
.on(
|
|
206
|
+
'postgres_changes',
|
|
207
|
+
{ event: '*', schema: 'public', table: 'todos' },
|
|
208
|
+
(payload) => refreshFromChange(payload),
|
|
209
|
+
)
|
|
210
|
+
.on('broadcast', { event: 'cursor' }, ({ payload }) => drawCursor(payload))
|
|
211
|
+
.on('presence', { event: 'sync' }, () => renderRoster(room.presenceState()))
|
|
212
|
+
.subscribe((status) => {
|
|
213
|
+
if (status === 'SUBSCRIBED') void room.track({ online: true });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await room.send({ type: 'broadcast', event: 'cursor', payload: { x, y } });
|
|
217
|
+
// On component/page cleanup:
|
|
218
|
+
mortar.removeChannel(room);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Clean up channels with their page/component. Persist through `from(table)`;
|
|
222
|
+
realtime only notifies peers.
|
|
223
|
+
|
|
224
|
+
## Compute and analytics
|
|
225
|
+
|
|
226
|
+
Invoke only functions already deployed through `mortar_migrate`:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const response = await mortar.compute.invoke({
|
|
230
|
+
name: 'summarize',
|
|
231
|
+
method: 'POST',
|
|
232
|
+
body: JSON.stringify({ text }),
|
|
233
|
+
contentType: 'application/json',
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) throw new Error(await response.text());
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Analytics calls are fire-and-forget except `flush`:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
mortar.analytics.track('pageview', { path: location.pathname });
|
|
242
|
+
mortar.analytics.identify(user.id, { plan: 'free' });
|
|
243
|
+
mortar.analytics.captureError(error, { screen: 'checkout' });
|
|
244
|
+
await mortar.analytics.flush();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Boundary for non-Fabric development
|
|
248
|
+
|
|
249
|
+
For Mortar itself or a standalone non-Fabric integration, inspect the current SDK
|
|
250
|
+
README. Never copy its standalone `createClient` setup into a Fabric project.
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mortar-ai/skill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code skill package for building on Mortar (BaaS). Drop `mortar-skill.md` into ~/.claude/skills/mortar/ to give Claude expert-level guidance about the Mortar SDK / CLI / MCP, tenant isolation + auth.tenant_id() RLS, and the 9 primitive × feature model.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": ["mortar-skill.md", "index.js", "README.md"],
|
|
7
|
+
"bin": {
|
|
8
|
+
"mortar-skill-install": "bin/install.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": { "access": "public" },
|
|
11
|
+
"license": "Apache-2.0"
|
|
12
|
+
}
|