@exposify/supabase 0.1.0 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,7 +15,7 @@ root.
15
15
 
16
16
  - A Supabase project (local via the [Supabase CLI](https://supabase.com/docs/guides/cli), or hosted).
17
17
  - The Supabase CLI (`supabase` on your `PATH`) for `db push` / `functions deploy`.
18
- - Node or Yarn to run `npx agentify-supabase` (the CLI is a regular npm
18
+ - Node or Yarn to run `npx exposify-supabase` (the CLI is a regular npm
19
19
  package; the edge function itself runs on Deno and needs no local install).
20
20
 
21
21
  ## 1. Scaffold
@@ -23,13 +23,13 @@ root.
23
23
  From your Supabase project's root (the directory containing `supabase/`):
24
24
 
25
25
  ```bash
26
- npx agentify-supabase init
26
+ npx exposify-supabase init
27
27
  ```
28
28
 
29
29
  This writes two things and never overwrites an existing function file:
30
30
 
31
- - `supabase/migrations/<timestamp>_agentify_meta.sql` — a SQL migration that
32
- adds `agentify_meta()`, a `SECURITY DEFINER` function returning, per table,
31
+ - `supabase/migrations/<timestamp>_exposify_meta.sql` — a SQL migration that
32
+ adds `exposify_meta()`, a `SECURITY DEFINER` function returning, per table,
33
33
  whether RLS is enabled and any `COMMENT ON TABLE`/`COMMENT ON COLUMN` text
34
34
  you've written. It only ever returns catalog metadata (table/column names,
35
35
  flags, comments) — never row data — which is what makes it safe to grant
@@ -39,8 +39,9 @@ This writes two things and never overwrites an existing function file:
39
39
  - `supabase/functions/mcp/docs/` — an empty directory for optional bundled
40
40
  docs (see "Knowledge base" below).
41
41
 
42
- `init` is interactive from there. If the Supabase CLI is on your `PATH` and
43
- you're running it in a real terminal, it asks:
42
+ `init` is interactive from there (skips straight to the old fixed behavior
43
+ below if stdin isn't a real terminal — CI, a script, piped input). If the
44
+ Supabase CLI is on your `PATH`, it asks:
44
45
 
45
46
  ```
46
47
  Apply the migration and deploy the function to whatever Supabase project is
@@ -49,8 +50,8 @@ currently linked, now? [y/N]
49
50
 
50
51
  Answering **y** runs `supabase db push` then `supabase functions deploy mcp`
51
52
  for you, streaming their output. The default is **no** — pressing Enter, the
52
- Supabase CLI not being installed, or running `init` non-interactively (CI, a
53
- script, piped input) all skip straight to printing the commands instead:
53
+ Supabase CLI not being installed, or running `init` non-interactively all
54
+ skip straight to printing the commands instead:
54
55
 
55
56
  ```
56
57
  Run these to finish setup:
@@ -61,14 +62,50 @@ Run these to finish setup:
61
62
 
62
63
  That's deliberate: `db push`/`functions deploy` act on whatever project is
63
64
  *currently linked*, which may not be a throwaway one, so nothing gets pushed
64
- without an explicit yes. If you want to edit `writes`/`rpc`/`functions` (next
65
- section) before the first deploy, just say no here and run the printed
66
- commands yourself once you're done editing.
65
+ without an explicit yes.
66
+
67
+ ### The setup wizard
68
+
69
+ If `init` can reach the project you're setting up — either because it was
70
+ already deployed (a re-run), or because you say yes above and the deploy
71
+ succeeds — it walks through a short interactive wizard instead of leaving
72
+ you to hand-edit `writes`/`rpc`/comments yourself:
73
+
74
+ 1. **Anonymous sign-in.** `init` tries to auto-detect this via the project's
75
+ public `/auth/v1/settings` endpoint and asks you to confirm; if it can't
76
+ detect it, it just asks. If enabled, it adds a warning comment to the
77
+ generated `index.ts`: an anonymous Supabase Auth session gets
78
+ `role: authenticated` (not `role: anon`), so it passes any RLS policy
79
+ scoped to `authenticated` unless that policy explicitly excludes
80
+ `auth.jwt()->>'is_anonymous' = 'true'`. Worth knowing before granting
81
+ `writes` on anything an anonymous session could reach.
82
+ 2. **Per-table writes.** For every RLS-enabled table, asks which of
83
+ insert/update/delete to grant (`i`, `u`, `d`, comma-separated, or Enter
84
+ for none) — the resulting `writes` array is written for you instead of
85
+ left as a commented-out example.
86
+ 3. **RPC functions.** Lists Postgres functions PostgREST exposes and lets
87
+ you pick which to expose as tools.
88
+ 4. **Documentation.** For every table or column `exposify_meta()` reports
89
+ has no comment, offers to write one right there — the answers become a
90
+ `COMMENT ON TABLE`/`COMMENT ON COLUMN` migration
91
+ (`<timestamp>_exposify_comments.sql`) that gets applied alongside the
92
+ rest. This is what gives `query_<table>`/`insert_<table>`/etc. their tool
93
+ and parameter descriptions (see "Column/table descriptions" below) —
94
+ worth doing even for tables you're not granting writes on, since it's
95
+ the only thing that makes read tools self-explanatory to an agent.
96
+
97
+ If `supabase/functions/mcp/index.ts` already exists, none of this touches
98
+ it — your selections are printed as a config snippet to merge in by hand
99
+ instead, and the documentation migration still gets written either way.
100
+ Connects using `SUPABASE_URL`/`SUPABASE_ANON_KEY` (env vars or `--url`/`--key`
101
+ flags) — without those, `init` can't reach anything to introspect and falls
102
+ back to the fixed scaffold-and-ask-to-deploy flow above, same as before this
103
+ wizard existed.
67
104
 
68
105
  ## 2. Decide what's exposed
69
106
 
70
107
  **Every RLS-enabled table is automatically readable.** A table only shows up
71
- at all if `agentify_meta()` reports RLS is turned on for it *and* PostgREST
108
+ at all if `exposify_meta()` reports RLS is turned on for it *and* PostgREST
72
109
  actually exposes it (the calling role has grants) — an RLS-less table is
73
110
  invisible, full stop, with no override. For each exposed table you get a
74
111
  `query_<table>` tool: typed filters (`eq`/`neq`/`gt`/`lt`/`like`), a `select`
@@ -136,7 +173,7 @@ supabase functions deploy mcp
136
173
 
137
174
  Only `functions deploy` is needed for a config-only change to `index.ts`;
138
175
  `db push` is only needed again if the migration itself changed (it hasn't,
139
- unless you're editing `sql/agentify_meta.sql` directly).
176
+ unless you're editing `sql/exposify_meta.sql` directly).
140
177
 
141
178
  (Local dev: `supabase start` then `supabase functions serve mcp` instead of
142
179
  `deploy`.)
@@ -169,9 +206,9 @@ them via the `search_docs` tool.
169
206
  Every request tries, in order:
170
207
 
171
208
  1. **Runtime introspection** — fetch PostgREST's OpenAPI document and call
172
- `agentify_meta()`. This is the normal path; the result is cached in
209
+ `exposify_meta()`. This is the normal path; the result is cached in
173
210
  memory for the life of the edge instance, not refetched per call.
174
- 2. **Bundled manifest** — if step 1 fails, and a `agentify.manifest.json`
211
+ 2. **Bundled manifest** — if step 1 fails, and a `exposify.manifest.json`
175
212
  was generated ahead of time (see below), tools are built from that
176
213
  instead.
177
214
  3. **Functions + knowledge base only** — if neither works, the function
@@ -187,32 +224,38 @@ project (local `supabase start` or linked):
187
224
 
188
225
  ```bash
189
226
  SUPABASE_URL=http://localhost:54321 \
190
- SUPABASE_SERVICE_ROLE_KEY=<service-role-key> \
191
- npx agentify-supabase generate
227
+ SUPABASE_ANON_KEY=<anon-key> \
228
+ npx exposify-supabase generate
192
229
  ```
193
230
 
194
- This writes `supabase/functions/mcp/agentify.manifest.json`. Load it in
231
+ Uses the **anon key**, deliberately the same key runtime introspection
232
+ itself uses (see "Fallback ladder" above) — a service-role key would see
233
+ tables/columns the `anon`/`authenticated` roles can't actually reach,
234
+ producing a fallback manifest that offers tools live introspection never
235
+ would.
236
+
237
+ This writes `supabase/functions/mcp/exposify.manifest.json`. Load it in
195
238
  `aiReady({ manifest: ... })` — since the function runs on Deno, import it
196
239
  with a JSON import:
197
240
 
198
241
  ```ts
199
- import manifest from "./agentify.manifest.json" with { type: "json" };
242
+ import manifest from "./exposify.manifest.json" with { type: "json" };
200
243
 
201
244
  Deno.serve(aiReady({ manifest, /* ...writes/rpc/functions... */ }));
202
245
  ```
203
246
 
204
247
  ## Troubleshooting
205
248
 
206
- **"agentify_meta() is not reachable" / "PGRST202" errors in function logs.**
249
+ **"exposify_meta() is not reachable" / "PGRST202" errors in function logs.**
207
250
  The migration hasn't been applied to this database. Run `supabase db push`
208
- (or apply `sql/agentify_meta.sql` from this package directly), then
251
+ (or apply `sql/exposify_meta.sql` from this package directly), then
209
252
  redeploy the function — the error message names this exact fix.
210
253
 
211
254
  **A table I expected doesn't show up as a tool.** Check, in order: is RLS
212
255
  actually enabled on it (`alter table ... enable row level security;`)? Does
213
256
  the calling role (`anon`/`authenticated`) have grants PostgREST can see? If
214
257
  both are true and it's still missing, confirm the migration is applied —
215
- `agentify_meta()` is what reports the RLS flag in the first place.
258
+ `exposify_meta()` is what reports the RLS flag in the first place.
216
259
 
217
260
  **An `rpc_<fn>` tool takes a single opaque `args` object instead of named
218
261
  parameters.** PostgREST's OpenAPI document didn't have a recognizable
@@ -226,3 +269,12 @@ entries are `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` /
226
269
  `"<table>.*"` — a malformed entry is dropped (with a console warning), not
227
270
  an error, so a typo silently disables just that grant rather than crashing
228
271
  the function.
272
+
273
+ **`init` just scaffolds and asks to deploy — it never asks about writes/RPCs/
274
+ documentation.** The wizard only runs when `init` can actually reach the
275
+ project (`SUPABASE_URL`/`SUPABASE_ANON_KEY` env vars, or `--url`/`--key`
276
+ flags, pointing at a project where the migration is already applied). On a
277
+ brand-new project's first `init`, that's not true yet — say yes to the
278
+ deploy prompt (or run `supabase db push` yourself) and the wizard picks up
279
+ automatically once the migration is live, in the same run if you said yes,
280
+ or on your next `exposify-supabase init` otherwise.
package/dist/cli.js CHANGED
@@ -19,31 +19,31 @@ async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
19
19
  }
20
20
 
21
21
  // src/meta.ts
22
- var MissingAgentifyMetaError = class extends Error {
22
+ var MissingExposifyMetaError = class extends Error {
23
23
  constructor(cause) {
24
24
  super(cause);
25
- this.name = "MissingAgentifyMetaError";
25
+ this.name = "MissingExposifyMetaError";
26
26
  }
27
27
  };
28
- async function fetchAgentifyMeta(postgrestUrl, headers, fetchImpl) {
29
- const res = await fetchImpl(`${postgrestUrl}/rpc/agentify_meta`, {
28
+ async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
29
+ const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
30
30
  method: "POST",
31
31
  headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
32
32
  body: "{}"
33
33
  });
34
34
  if (res.status === 404) {
35
- throw new MissingAgentifyMetaError(
36
- "agentify_meta() is not reachable at /rpc/agentify_meta (404). Run `npx agentify-supabase init` (or apply sql/agentify_meta.sql directly) and `supabase db push`, then redeploy the function."
35
+ throw new MissingExposifyMetaError(
36
+ "exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
37
37
  );
38
38
  }
39
39
  if (!res.ok) {
40
40
  const body = await res.text().catch(() => "");
41
41
  if (/PGRST202|function .* does not exist/i.test(body)) {
42
- throw new MissingAgentifyMetaError(
43
- "agentify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/agentify_meta.sql via `supabase db push`, then redeploy the function."
42
+ throw new MissingExposifyMetaError(
43
+ "exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
44
44
  );
45
45
  }
46
- throw new Error(`agentify_meta() call failed: HTTP ${res.status} ${body}`.trim());
46
+ throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
47
47
  }
48
48
  return await res.json();
49
49
  }
@@ -74,7 +74,7 @@ function columnsFromOpenApi(doc, table) {
74
74
  // ON COLUMN` into `description`, then appends its own "Note:\nThis is a
75
75
  // Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
76
76
  // so a column with no real comment ends up `undefined`, not the
77
- // boilerplate text. agentify_meta()'s copy wins when both exist anyway
77
+ // boilerplate text. exposify_meta()'s copy wins when both exist anyway
78
78
  // -- see introspect.ts's merge.
79
79
  description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
80
80
  }));
@@ -99,7 +99,7 @@ function introspectTables(openapi, metaRows) {
99
99
  if (!fromOpenApi) continue;
100
100
  columns.push({
101
101
  ...fromOpenApi,
102
- // agentify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
102
+ // exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
103
103
  // "Column comments → param descriptions" sourced from the migration).
104
104
  description: row.column_comment ?? fromOpenApi.description
105
105
  });
@@ -130,19 +130,98 @@ function buildManifest(openapi, metaRows, generatedAt) {
130
130
  return { version: MANIFEST_VERSION, generatedAt, tables: introspectTables(openapi, metaRows) };
131
131
  }
132
132
 
133
+ // bin/wizard.ts
134
+ function parseAnonymousSignInSetting(settings) {
135
+ if (!settings || typeof settings !== "object") return null;
136
+ const s = settings;
137
+ const external = s.external;
138
+ if (external && typeof external.anonymous_users === "boolean") return external.anonymous_users;
139
+ if (typeof s.anonymous_users === "boolean") return s.anonymous_users;
140
+ if (typeof s.external_anonymous_users === "boolean") return s.external_anonymous_users;
141
+ return null;
142
+ }
143
+ function rpcCandidatesFromOpenApi(doc) {
144
+ return Object.keys(doc.paths ?? {}).filter((p) => p.startsWith("/rpc/")).map((p) => p.slice("/rpc/".length)).filter((name) => name.length > 0 && name !== "exposify_meta").sort();
145
+ }
146
+ function flattenWriteGrants(grants) {
147
+ return grants.flatMap((g) => g.ops.map((op) => `${g.table}.${op}`));
148
+ }
149
+ function parseWriteOpsAnswer(answer) {
150
+ const seen = /* @__PURE__ */ new Set();
151
+ for (const raw of answer.split(",")) {
152
+ const token = raw.trim().toLowerCase();
153
+ if (token === "i" || token === "insert") seen.add("insert");
154
+ else if (token === "u" || token === "update") seen.add("update");
155
+ else if (token === "d" || token === "delete") seen.add("delete");
156
+ }
157
+ return ["insert", "update", "delete"].filter((op) => seen.has(op));
158
+ }
159
+ function parseSelectionAnswer(answer, candidates) {
160
+ const wanted = new Set(
161
+ answer.split(",").map((s) => s.trim()).filter(Boolean)
162
+ );
163
+ return candidates.filter((c) => wanted.has(c));
164
+ }
165
+ function tablesMissingDocs(tables) {
166
+ return tables.map((t) => ({
167
+ name: t.name,
168
+ missingTableComment: !t.description,
169
+ columnsWithoutComments: t.columns.filter((c) => !c.description).map((c) => c.name)
170
+ }));
171
+ }
172
+ function sqlStringLiteral(text) {
173
+ return `'${text.replace(/'/g, "''")}'`;
174
+ }
175
+ function sqlIdentifier(name) {
176
+ return `"${name.replace(/"/g, '""')}"`;
177
+ }
178
+ function buildCommentStatements(entries) {
179
+ const lines = [];
180
+ for (const entry of entries) {
181
+ const table = sqlIdentifier(entry.table);
182
+ if (entry.tableDescription) {
183
+ lines.push(`comment on table public.${table} is ${sqlStringLiteral(entry.tableDescription)};`);
184
+ }
185
+ for (const col of entry.columns) {
186
+ lines.push(
187
+ `comment on column public.${table}.${sqlIdentifier(col.name)} is ${sqlStringLiteral(col.description)};`
188
+ );
189
+ }
190
+ }
191
+ return lines.length > 0 ? `${lines.join("\n")}
192
+ ` : "";
193
+ }
194
+ var ANONYMOUS_SIGN_IN_WARNING = `// NOTE: this project has anonymous sign-in enabled. An anonymous Supabase
195
+ // Auth session still gets role: authenticated (with an is_anonymous claim),
196
+ // NOT role: anon -- so it passes any RLS policy scoped to "authenticated"
197
+ // unless that policy explicitly excludes auth.jwt()->>'is_anonymous' = 'true'.
198
+ // Double-check your RLS policies (especially on tables granted writes below)
199
+ // account for this before trusting "authenticated" to mean "a real user".
200
+ `;
201
+ function buildFunctionTemplate(opts = {}) {
202
+ const lines = [];
203
+ if (opts.anonymousSignIn) lines.push(ANONYMOUS_SIGN_IN_WARNING);
204
+ lines.push(`import { aiReady } from "npm:@exposify/supabase";`, "");
205
+ lines.push("Deno.serve(aiReady({");
206
+ lines.push(
207
+ opts.writes && opts.writes.length > 0 ? ` writes: ${JSON.stringify(opts.writes)},` : ` // writes: ["orders.insert", "orders.update"],`
208
+ );
209
+ lines.push(
210
+ opts.rpc && opts.rpc.length > 0 ? ` rpc: ${JSON.stringify(opts.rpc)},` : ` // rpc: ["place_order"],`
211
+ );
212
+ lines.push(
213
+ " // functions: {",
214
+ ' // refund: { description: "Refund an order", input: { type: "object", properties: {} } },',
215
+ " // },",
216
+ "}));",
217
+ ""
218
+ );
219
+ return lines.join("\n");
220
+ }
221
+
133
222
  // bin/cli.ts
134
223
  var PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
135
224
  var FUNCTION_DIR = "supabase/functions/mcp";
136
- var FUNCTION_TEMPLATE = `import { aiReady } from "npm:@exposify/supabase";
137
-
138
- Deno.serve(aiReady({
139
- // writes: ["orders.insert", "orders.update"],
140
- // rpc: ["place_order"],
141
- // functions: {
142
- // refund: { description: "Refund an order", input: { type: "object", properties: {} } },
143
- // },
144
- }));
145
- `;
146
225
  async function exists(path) {
147
226
  try {
148
227
  await access(path);
@@ -159,15 +238,18 @@ function hasSupabaseCli() {
159
238
  return false;
160
239
  }
161
240
  }
162
- async function promptYesNo(question) {
163
- if (!process.stdin.isTTY) return false;
164
- const rl = createInterface({ input: process.stdin, output: process.stdout });
165
- try {
166
- const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
167
- return answer === "y" || answer === "yes";
168
- } finally {
169
- rl.close();
170
- }
241
+ function openInteractiveReadline() {
242
+ if (!process.stdin.isTTY) return null;
243
+ return createInterface({ input: process.stdin, output: process.stdout });
244
+ }
245
+ async function promptYesNo(rl, question, defaultYes = false) {
246
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
247
+ const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
248
+ if (answer === "") return defaultYes;
249
+ return answer === "y" || answer === "yes";
250
+ }
251
+ async function promptText(rl, question) {
252
+ return (await rl.question(question)).trim();
171
253
  }
172
254
  function printManualCommands(opts = {}) {
173
255
  console.log("\nRun these to finish setup:");
@@ -177,66 +259,250 @@ function printManualCommands(opts = {}) {
177
259
  steps.forEach((cmd, i) => console.log(` ${i + 1}. ${cmd}`));
178
260
  console.log(` ${steps.length + 1}. Paste the function URL + a user JWT into an MCP client.`);
179
261
  }
262
+ function pushAndDeploy() {
263
+ let pushed = false;
264
+ try {
265
+ console.log("\n$ supabase db push");
266
+ execFileSync("supabase", ["db", "push"], { stdio: "inherit" });
267
+ pushed = true;
268
+ console.log("\n$ supabase functions deploy mcp");
269
+ execFileSync("supabase", ["functions", "deploy", "mcp"], { stdio: "inherit" });
270
+ return { pushed: true, deployed: true };
271
+ } catch (err) {
272
+ return { pushed, deployed: false, error: err };
273
+ }
274
+ }
275
+ function resolveConnectionInfo() {
276
+ const url = process.env.SUPABASE_URL ?? argValue("--url");
277
+ const anonKey = process.env.SUPABASE_ANON_KEY ?? argValue("--key");
278
+ if (!url || !anonKey) return null;
279
+ return { url, anonKey };
280
+ }
281
+ async function tryIntrospect(conn) {
282
+ try {
283
+ const postgrestUrl = `${conn.url.replace(/\/+$/, "")}/rest/v1`;
284
+ const headers = new Headers({ authorization: `Bearer ${conn.anonKey}`, apikey: conn.anonKey });
285
+ const [openapi, metaRows] = await Promise.all([
286
+ fetchOpenApi(postgrestUrl, headers, fetch),
287
+ fetchExposifyMeta(postgrestUrl, headers, fetch)
288
+ ]);
289
+ return { tables: introspectTables(openapi, metaRows), openapi };
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+ async function tryDetectAnonymousSignIn(conn) {
295
+ try {
296
+ const url = `${conn.url.replace(/\/+$/, "")}/auth/v1/settings`;
297
+ const res = await fetch(url, { headers: { apikey: conn.anonKey, authorization: `Bearer ${conn.anonKey}` } });
298
+ if (!res.ok) return null;
299
+ return parseAnonymousSignInSetting(await res.json());
300
+ } catch {
301
+ return null;
302
+ }
303
+ }
304
+ async function askAnonymousSignIn(rl, conn) {
305
+ const detected = conn ? await tryDetectAnonymousSignIn(conn) : null;
306
+ if (detected !== null) {
307
+ console.log(`
308
+ Detected: anonymous sign-in is ${detected ? "enabled" : "disabled"} on this project.`);
309
+ const confirmed = await promptYesNo(rl, "Is that correct?", true);
310
+ if (confirmed) return detected;
311
+ }
312
+ return promptYesNo(rl, "\nDoes this Supabase project have anonymous sign-in enabled (Authentication \u2192 Sign In / Providers \u2192 Anonymous)?");
313
+ }
314
+ async function askWriteGrants(rl, tables) {
315
+ if (tables.length === 0) return [];
316
+ const configure = await promptYesNo(rl, `
317
+ Configure write access for ${tables.length} table(s)?`);
318
+ if (!configure) return [];
319
+ const grants = [];
320
+ for (const table of tables) {
321
+ const answer = await promptText(
322
+ rl,
323
+ ` ${table.name} -- grant writes? (i=insert, u=update, d=delete, comma-separate, Enter=none): `
324
+ );
325
+ const ops = parseWriteOpsAnswer(answer);
326
+ if (ops.length > 0) grants.push({ table: table.name, ops });
327
+ }
328
+ return grants;
329
+ }
330
+ async function askRpcSelection(rl, candidates) {
331
+ if (candidates.length === 0) return [];
332
+ console.log(`
333
+ Available Postgres functions: ${candidates.join(", ")}`);
334
+ const answer = await promptText(rl, "Expose any as tools? (comma-separate, Enter=none): ");
335
+ return parseSelectionAnswer(answer, candidates);
336
+ }
337
+ async function askDocumentation(rl, tables) {
338
+ const missing = tablesMissingDocs(tables).filter((t) => t.missingTableComment || t.columnsWithoutComments.length > 0);
339
+ if (missing.length === 0) return [];
340
+ const configure = await promptYesNo(
341
+ rl,
342
+ `
343
+ ${missing.length} table(s) are missing documentation MCP tools would otherwise show agents -- write some now?`
344
+ );
345
+ if (!configure) return [];
346
+ const entries = [];
347
+ for (const t of missing) {
348
+ const entry = { table: t.name, columns: [] };
349
+ if (t.missingTableComment) {
350
+ const desc = await promptText(rl, ` ${t.name} -- what is this table for? (Enter to skip): `);
351
+ if (desc) entry.tableDescription = desc;
352
+ }
353
+ if (t.columnsWithoutComments.length > 0) {
354
+ const addColumns = await promptYesNo(
355
+ rl,
356
+ ` Add descriptions for ${t.name}'s ${t.columnsWithoutComments.length} undocumented column(s)?`
357
+ );
358
+ if (addColumns) {
359
+ for (const col of t.columnsWithoutComments) {
360
+ const desc = await promptText(rl, ` ${t.name}.${col}: `);
361
+ if (desc) entry.columns.push({ name: col, description: desc });
362
+ }
363
+ }
364
+ }
365
+ if (entry.tableDescription || entry.columns.length > 0) entries.push(entry);
366
+ }
367
+ return entries;
368
+ }
369
+ async function writeCommentsMigration(entries) {
370
+ const sql = buildCommentStatements(entries);
371
+ if (!sql) return null;
372
+ const migrationsDir = "supabase/migrations";
373
+ await mkdir(migrationsDir, { recursive: true });
374
+ const path = join(migrationsDir, `${timestamp()}_exposify_comments.sql`);
375
+ await writeFile(path, sql);
376
+ return path;
377
+ }
180
378
  async function cmdInit() {
181
- const migrationSql = await readFile(join(PACKAGE_ROOT, "sql/agentify_meta.sql"), "utf8");
379
+ const migrationSql = await readFile(join(PACKAGE_ROOT, "sql/exposify_meta.sql"), "utf8");
182
380
  const migrationsDir = "supabase/migrations";
183
381
  await mkdir(migrationsDir, { recursive: true });
184
- const migrationPath = join(migrationsDir, `${timestamp()}_agentify_meta.sql`);
382
+ const migrationPath = join(migrationsDir, `${timestamp()}_exposify_meta.sql`);
185
383
  await writeFile(migrationPath, migrationSql);
186
384
  console.log(`Wrote ${migrationPath}`);
187
385
  await mkdir(FUNCTION_DIR, { recursive: true });
188
386
  const functionPath = join(FUNCTION_DIR, "index.ts");
189
- if (await exists(functionPath)) {
190
- console.log(`Skipped ${functionPath} (already exists)`);
191
- } else {
192
- await writeFile(functionPath, FUNCTION_TEMPLATE);
193
- console.log(`Wrote ${functionPath}`);
194
- }
387
+ const functionExists = await exists(functionPath);
195
388
  await mkdir(join(FUNCTION_DIR, "docs"), { recursive: true });
196
- if (!hasSupabaseCli()) {
197
- console.log("\nSupabase CLI not found on PATH -- install it to run the next steps automatically next time.");
198
- printManualCommands();
199
- return;
200
- }
201
- const connect = await promptYesNo(
202
- "\nApply the migration and deploy the function to whatever Supabase project is currently linked, now?"
203
- );
204
- if (!connect) {
389
+ const rl = openInteractiveReadline();
390
+ if (!rl) {
391
+ if (!functionExists) {
392
+ await writeFile(functionPath, buildFunctionTemplate());
393
+ console.log(`Wrote ${functionPath}`);
394
+ } else {
395
+ console.log(`Skipped ${functionPath} (already exists)`);
396
+ }
205
397
  printManualCommands();
206
398
  return;
207
399
  }
208
- let pushed = false;
209
400
  try {
210
- console.log("\n$ supabase db push");
211
- execFileSync("supabase", ["db", "push"], { stdio: "inherit" });
212
- pushed = true;
213
- console.log("\n$ supabase functions deploy mcp");
214
- execFileSync("supabase", ["functions", "deploy", "mcp"], { stdio: "inherit" });
401
+ const conn = resolveConnectionInfo();
402
+ const anonymousSignIn = await askAnonymousSignIn(rl, conn);
403
+ let introspection = conn ? await tryIntrospect(conn) : null;
404
+ let alreadyPushed = false;
405
+ let alreadyDeployed = false;
406
+ if (!introspection) {
407
+ if (!functionExists) {
408
+ await writeFile(functionPath, buildFunctionTemplate({ anonymousSignIn }));
409
+ console.log(`Wrote ${functionPath}`);
410
+ } else {
411
+ console.log(`Skipped ${functionPath} (already exists)`);
412
+ }
413
+ if (!hasSupabaseCli()) {
414
+ console.log("\nSupabase CLI not found on PATH -- install it to run the next steps automatically next time.");
415
+ printManualCommands();
416
+ return;
417
+ }
418
+ const connect = await promptYesNo(
419
+ rl,
420
+ "\nApply the migration and deploy the function to whatever Supabase project is currently linked, now?"
421
+ );
422
+ if (!connect) {
423
+ printManualCommands();
424
+ return;
425
+ }
426
+ const result2 = pushAndDeploy();
427
+ alreadyPushed = result2.pushed;
428
+ alreadyDeployed = result2.deployed;
429
+ if (!result2.deployed) {
430
+ console.error(`
431
+ Supabase CLI command failed: ${result2.error instanceof Error ? result2.error.message : result2.error}`);
432
+ printManualCommands({ alreadyPushed });
433
+ return;
434
+ }
435
+ introspection = conn ? await tryIntrospect(conn) : null;
436
+ if (!introspection) {
437
+ console.log(
438
+ "\nDeployed, but couldn't reach the project to introspect tables (no SUPABASE_URL/SUPABASE_ANON_KEY or --url/--key available). Re-run `exposify-supabase init` with those set to configure writes, RPCs, and documentation interactively."
439
+ );
440
+ console.log("\nDone. Paste the function URL + a user JWT into an MCP client to try it.");
441
+ return;
442
+ }
443
+ }
444
+ const grants = await askWriteGrants(rl, introspection.tables);
445
+ const rpcCandidates = rpcCandidatesFromOpenApi(introspection.openapi);
446
+ const rpc = await askRpcSelection(rl, rpcCandidates);
447
+ const docs = await askDocumentation(rl, introspection.tables);
448
+ const writes = flattenWriteGrants(grants);
449
+ let wroteFunctionFile = false;
450
+ if (!functionExists) {
451
+ await writeFile(functionPath, buildFunctionTemplate({ writes, rpc, anonymousSignIn }));
452
+ console.log(`
453
+ Wrote ${functionPath} with your selections.`);
454
+ wroteFunctionFile = true;
455
+ } else if (writes.length > 0 || rpc.length > 0) {
456
+ console.log(`
457
+ ${functionPath} already exists -- left untouched. Merge this into your aiReady() config:`);
458
+ console.log(buildFunctionTemplate({ writes, rpc, anonymousSignIn }));
459
+ }
460
+ const commentsPath = await writeCommentsMigration(docs);
461
+ if (commentsPath) console.log(`Wrote ${commentsPath}`);
462
+ const needsDeploy = wroteFunctionFile || commentsPath !== null;
463
+ if (!needsDeploy) {
464
+ console.log("\nNothing new to deploy.");
465
+ return;
466
+ }
467
+ if (!hasSupabaseCli()) {
468
+ console.log("\nSupabase CLI not found on PATH.");
469
+ printManualCommands({ alreadyPushed: commentsPath === null, alreadyDeployed: !wroteFunctionFile });
470
+ return;
471
+ }
472
+ const redeploy = await promptYesNo(rl, "\nApply/deploy these changes now?", true);
473
+ if (!redeploy) {
474
+ printManualCommands({ alreadyPushed: commentsPath === null, alreadyDeployed: !wroteFunctionFile });
475
+ return;
476
+ }
477
+ const result = pushAndDeploy();
478
+ if (!result.deployed) {
479
+ console.error(`
480
+ Supabase CLI command failed: ${result.error instanceof Error ? result.error.message : result.error}`);
481
+ printManualCommands({ alreadyPushed: result.pushed });
482
+ return;
483
+ }
215
484
  console.log("\nDone. Paste the function URL + a user JWT into an MCP client to try it.");
216
- } catch (err) {
217
- console.error(`
218
- Supabase CLI command failed: ${err instanceof Error ? err.message : err}`);
219
- printManualCommands({ alreadyPushed: pushed });
485
+ } finally {
486
+ rl.close();
220
487
  }
221
488
  }
222
489
  async function cmdGenerate() {
223
- const supabaseUrl = process.env.SUPABASE_URL ?? argValue("--url");
224
- const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? argValue("--key");
225
- if (!supabaseUrl || !serviceKey) {
490
+ const conn = resolveConnectionInfo();
491
+ if (!conn) {
226
492
  console.error(
227
- "generate needs SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (env vars, or --url/--key flags) pointing at a running local or linked project (e.g. `supabase start`)."
493
+ "generate needs SUPABASE_URL and SUPABASE_ANON_KEY (env vars, or --url/--key flags) pointing at a running local or linked project (e.g. `supabase start`)."
228
494
  );
229
495
  process.exitCode = 1;
230
496
  return;
231
497
  }
232
- const postgrestUrl = `${supabaseUrl.replace(/\/+$/, "")}/rest/v1`;
233
- const headers = new Headers({ authorization: `Bearer ${serviceKey}`, apikey: serviceKey });
498
+ const postgrestUrl = `${conn.url.replace(/\/+$/, "")}/rest/v1`;
499
+ const headers = new Headers({ authorization: `Bearer ${conn.anonKey}`, apikey: conn.anonKey });
234
500
  const [openapi, metaRows] = await Promise.all([
235
501
  fetchOpenApi(postgrestUrl, headers, fetch),
236
- fetchAgentifyMeta(postgrestUrl, headers, fetch)
502
+ fetchExposifyMeta(postgrestUrl, headers, fetch)
237
503
  ]);
238
504
  const manifest = buildManifest(openapi, metaRows, (/* @__PURE__ */ new Date()).toISOString());
239
- const outPath = join(FUNCTION_DIR, "agentify.manifest.json");
505
+ const outPath = join(FUNCTION_DIR, "exposify.manifest.json");
240
506
  await mkdir(dirname(outPath), { recursive: true });
241
507
  await writeFile(outPath, JSON.stringify(manifest, null, 2));
242
508
  console.log(`Wrote ${outPath} (${manifest.tables.length} table(s))`);
@@ -254,10 +520,13 @@ async function main() {
254
520
  const [, , cmd] = process.argv;
255
521
  if (cmd === "init") return cmdInit();
256
522
  if (cmd === "generate") return cmdGenerate();
257
- console.log("Usage: agentify-supabase <init|generate>");
258
- console.log(" init Scaffold the agentify_meta() migration + supabase/functions/mcp/index.ts,");
259
- console.log(" then interactively offer to run `supabase db push` + `functions deploy`");
260
- console.log(" generate Build agentify.manifest.json from a running local/linked project");
523
+ console.log("Usage: exposify-supabase <init|generate> [--url <url>] [--key <anon-key>]");
524
+ console.log(" init Scaffold the exposify_meta() migration + supabase/functions/mcp/index.ts.");
525
+ console.log(" Interactively offers to run `supabase db push` + `functions deploy`; if a");
526
+ console.log(" project is reachable (SUPABASE_URL/SUPABASE_ANON_KEY env vars, or --url/--key),");
527
+ console.log(" also walks through anonymous sign-in, per-table writes, RPC exposure, and");
528
+ console.log(" documentation for tables/columns missing a comment.");
529
+ console.log(" generate Build exposify.manifest.json from a running local/linked project");
261
530
  process.exitCode = cmd ? 1 : 0;
262
531
  }
263
532
  main().catch((err) => {
package/dist/index.cjs CHANGED
@@ -21,11 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  MANIFEST_VERSION: () => MANIFEST_VERSION,
24
- MissingAgentifyMetaError: () => MissingAgentifyMetaError,
24
+ MissingExposifyMetaError: () => MissingExposifyMetaError,
25
25
  aiReady: () => aiReady,
26
26
  buildManifest: () => buildManifest,
27
27
  columnsFromOpenApi: () => columnsFromOpenApi,
28
- fetchAgentifyMeta: () => fetchAgentifyMeta,
28
+ fetchExposifyMeta: () => fetchExposifyMeta,
29
29
  fetchOpenApi: () => fetchOpenApi,
30
30
  introspectTables: () => introspectTables,
31
31
  loadManifest: () => loadManifest,
@@ -103,7 +103,7 @@ function warnOnce(message) {
103
103
  if (warned.has(message)) return;
104
104
  warned.add(message);
105
105
  try {
106
- console.warn(`[agentify-supabase] ${message}`);
106
+ console.warn(`[exposify-supabase] ${message}`);
107
107
  } catch {
108
108
  }
109
109
  }
@@ -129,6 +129,7 @@ function buildPostgrestHeaders(incoming, anonKey) {
129
129
  if (value) headers.set(name, value);
130
130
  }
131
131
  if (!headers.has("apikey")) headers.set("apikey", anonKey);
132
+ if (!headers.has("authorization")) headers.set("authorization", `Bearer ${anonKey}`);
132
133
  return headers;
133
134
  }
134
135
  function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
@@ -172,31 +173,31 @@ async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
172
173
  }
173
174
 
174
175
  // src/meta.ts
175
- var MissingAgentifyMetaError = class extends Error {
176
+ var MissingExposifyMetaError = class extends Error {
176
177
  constructor(cause) {
177
178
  super(cause);
178
- this.name = "MissingAgentifyMetaError";
179
+ this.name = "MissingExposifyMetaError";
179
180
  }
180
181
  };
181
- async function fetchAgentifyMeta(postgrestUrl, headers, fetchImpl) {
182
- const res = await fetchImpl(`${postgrestUrl}/rpc/agentify_meta`, {
182
+ async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
183
+ const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
183
184
  method: "POST",
184
185
  headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
185
186
  body: "{}"
186
187
  });
187
188
  if (res.status === 404) {
188
- throw new MissingAgentifyMetaError(
189
- "agentify_meta() is not reachable at /rpc/agentify_meta (404). Run `npx agentify-supabase init` (or apply sql/agentify_meta.sql directly) and `supabase db push`, then redeploy the function."
189
+ throw new MissingExposifyMetaError(
190
+ "exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
190
191
  );
191
192
  }
192
193
  if (!res.ok) {
193
194
  const body = await res.text().catch(() => "");
194
195
  if (/PGRST202|function .* does not exist/i.test(body)) {
195
- throw new MissingAgentifyMetaError(
196
- "agentify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/agentify_meta.sql via `supabase db push`, then redeploy the function."
196
+ throw new MissingExposifyMetaError(
197
+ "exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
197
198
  );
198
199
  }
199
- throw new Error(`agentify_meta() call failed: HTTP ${res.status} ${body}`.trim());
200
+ throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
200
201
  }
201
202
  return await res.json();
202
203
  }
@@ -227,7 +228,7 @@ function columnsFromOpenApi(doc, table) {
227
228
  // ON COLUMN` into `description`, then appends its own "Note:\nThis is a
228
229
  // Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
229
230
  // so a column with no real comment ends up `undefined`, not the
230
- // boilerplate text. agentify_meta()'s copy wins when both exist anyway
231
+ // boilerplate text. exposify_meta()'s copy wins when both exist anyway
231
232
  // -- see introspect.ts's merge.
232
233
  description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
233
234
  }));
@@ -262,7 +263,7 @@ function introspectTables(openapi, metaRows) {
262
263
  if (!fromOpenApi) continue;
263
264
  columns.push({
264
265
  ...fromOpenApi,
265
- // agentify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
266
+ // exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
266
267
  // "Column comments → param descriptions" sourced from the migration).
267
268
  description: row.column_comment ?? fromOpenApi.description
268
269
  });
@@ -305,7 +306,7 @@ async function resolveTableSchemas(deps) {
305
306
  try {
306
307
  const [openapi, metaRows] = await Promise.all([
307
308
  fetchOpenApi(deps.postgrestUrl, deps.headers, deps.fetchImpl),
308
- fetchAgentifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
309
+ fetchExposifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
309
310
  ]);
310
311
  return { tables: introspectTables(openapi, metaRows), mode: "runtime", warnings, openapi };
311
312
  } catch (err) {
@@ -315,7 +316,7 @@ async function resolveTableSchemas(deps) {
315
316
  if (manifest) {
316
317
  warnings.push({
317
318
  rung: "manifest",
318
- reason: "runtime introspection failed; serving tools from the bundled agentify.manifest.json instead"
319
+ reason: "runtime introspection failed; serving tools from the bundled exposify.manifest.json instead"
319
320
  });
320
321
  return { tables: manifest.tables, mode: "manifest", warnings };
321
322
  }
@@ -602,9 +603,9 @@ ${h.chunk.text}`).join("\n\n---\n\n") : "No matching documentation found.";
602
603
  function registerDiagnosticsResource(server, warnings) {
603
604
  if (warnings.length === 0) return;
604
605
  server.registerResource(
605
- "agentify-diagnostics",
606
- "agentify://diagnostics",
607
- { title: "agentify-supabase diagnostics", mimeType: "application/json" },
606
+ "exposify-diagnostics",
607
+ "exposify://diagnostics",
608
+ { title: "exposify-supabase diagnostics", mimeType: "application/json" },
608
609
  async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(warnings, null, 2) }] })
609
610
  );
610
611
  }
@@ -659,7 +660,7 @@ function aiReady(options = {}) {
659
660
  manifest: options.manifest
660
661
  }).then((ladder) => {
661
662
  for (const w of ladder.warnings) {
662
- console.error(`[agentify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
663
+ console.error(`[exposify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
663
664
  }
664
665
  return ladder;
665
666
  });
@@ -687,7 +688,7 @@ function aiReady(options = {}) {
687
688
  return { status: res.status, bodyText: await res.text() };
688
689
  };
689
690
  return buildSupabaseMcpServer({
690
- appName: "agentify-supabase",
691
+ appName: "exposify-supabase",
691
692
  appVersion: "0.0.0",
692
693
  tables: ladder.tables,
693
694
  cfg,
@@ -706,11 +707,11 @@ function aiReady(options = {}) {
706
707
  // Annotate the CommonJS export names for ESM import in node:
707
708
  0 && (module.exports = {
708
709
  MANIFEST_VERSION,
709
- MissingAgentifyMetaError,
710
+ MissingExposifyMetaError,
710
711
  aiReady,
711
712
  buildManifest,
712
713
  columnsFromOpenApi,
713
- fetchAgentifyMeta,
714
+ fetchExposifyMeta,
714
715
  fetchOpenApi,
715
716
  introspectTables,
716
717
  loadManifest,
package/dist/index.d.cts CHANGED
@@ -7,18 +7,18 @@ interface ColumnSchema {
7
7
  /** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
8
8
  format?: string;
9
9
  required: boolean;
10
- /** From `agentify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
10
+ /** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
11
11
  description?: string;
12
12
  isPrimaryKey?: boolean;
13
13
  }
14
14
  interface TableSchema {
15
15
  name: string;
16
16
  rlsEnabled: boolean;
17
- /** From `agentify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
17
+ /** From `exposify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
18
18
  description?: string;
19
19
  columns: ColumnSchema[];
20
20
  }
21
- /** One row of `agentify_meta()` -- the migration returns one row per column. */
21
+ /** One row of `exposify_meta()` -- the migration returns one row per column. */
22
22
  interface MetaRow {
23
23
  table_name: string;
24
24
  rls_enabled: boolean;
@@ -62,7 +62,7 @@ interface AgentifySupabaseOptions {
62
62
  anonKey?: string;
63
63
  /** Injectable for tests; defaults to global `fetch`. */
64
64
  fetchImpl?: typeof fetch;
65
- /** Bundled fallback manifest (from `agentify-supabase generate`), used when runtime introspection fails. */
65
+ /** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
66
66
  manifest?: unknown;
67
67
  rateLimit?: number;
68
68
  rateWindowMs?: number;
@@ -98,7 +98,7 @@ interface PostgrestColumnProperty {
98
98
  /**
99
99
  * Extracts column schemas for one table from a PostgREST OpenAPI document.
100
100
  * Returns `[]` for a table PostgREST never exposed (not in `definitions` --
101
- * e.g. it exists at the catalog level per `agentify_meta()` but PostgREST's
101
+ * e.g. it exists at the catalog level per `exposify_meta()` but PostgREST's
102
102
  * role lacks USAGE/SELECT grants on it), which the caller treats the same as
103
103
  * "RLS-less": never shown to the agent.
104
104
  */
@@ -113,27 +113,27 @@ declare function columnsFromOpenApi(doc: PostgrestOpenApiDoc, table: string): Co
113
113
  declare function rpcArgSchema(doc: PostgrestOpenApiDoc, fn: string): JsonSchemaObject | null;
114
114
 
115
115
  declare const MANIFEST_VERSION = 1;
116
- interface AgentifyManifest {
116
+ interface ExposifyManifest {
117
117
  version: typeof MANIFEST_VERSION;
118
118
  generatedAt: string;
119
119
  tables: TableSchema[];
120
120
  }
121
- /** Built by `npx agentify-supabase generate` against a local/linked project. */
122
- declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): AgentifyManifest;
121
+ /** Built by `npx exposify-supabase generate` against a local/linked project. */
122
+ declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): ExposifyManifest;
123
123
  /**
124
124
  * Defensive load: a manifest is bundled JSON shipped alongside the function
125
125
  * and can go stale relative to the deployed schema, or simply be malformed
126
126
  * (hand-edited, wrong version). Returns `null` -- never throws -- so the
127
127
  * degradation ladder can fall through to the next rung instead of crashing.
128
128
  */
129
- declare function loadManifest(raw: unknown): AgentifyManifest | null;
129
+ declare function loadManifest(raw: unknown): ExposifyManifest | null;
130
130
 
131
131
  /**
132
- * Merges PostgREST's OpenAPI document (column types) with `agentify_meta()`
132
+ * Merges PostgREST's OpenAPI document (column types) with `exposify_meta()`
133
133
  * rows (RLS status + comments) into the DB tool surface.
134
134
  *
135
135
  * DB exposure policy (spec, locked): RLS-gated, reads auto. A table only
136
- * appears here if `agentify_meta()` reports `rls_enabled: true` for it AND
136
+ * appears here if `exposify_meta()` reports `rls_enabled: true` for it AND
137
137
  * PostgREST actually exposes it in its OpenAPI `definitions` (the role
138
138
  * calling this has grants on it) -- a table failing either check never
139
139
  * appears, silently, exactly like a table PostgREST doesn't know about at
@@ -142,25 +142,26 @@ declare function loadManifest(raw: unknown): AgentifyManifest | null;
142
142
  */
143
143
  declare function introspectTables(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[]): TableSchema[];
144
144
 
145
- declare class MissingAgentifyMetaError extends Error {
145
+ declare class MissingExposifyMetaError extends Error {
146
146
  constructor(cause: string);
147
147
  }
148
148
  /**
149
- * Calls the `agentify_meta()` Postgres function via PostgREST's RPC
150
- * endpoint. Requires the migration from `sql/agentify_meta.sql` to have been
149
+ * Calls the `exposify_meta()` Postgres function via PostgREST's RPC
150
+ * endpoint. Requires the migration from `sql/exposify_meta.sql` to have been
151
151
  * applied; a 404 (unknown route) or PostgREST's `PGRST202` (function not
152
152
  * found in schema cache) both mean "migration missing" and are translated
153
153
  * into a self-diagnosing error (spec: "a missing `agentify_meta()` migration
154
- * produces a self-diagnosing message naming the exact fix").
154
+ * produces a self-diagnosing message naming the exact fix" -- function
155
+ * renamed to `exposify_meta()` post-spec, see the decisions journal).
155
156
  */
156
- declare function fetchAgentifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
157
+ declare function fetchExposifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
157
158
 
158
159
  /** PostgREST serves its OpenAPI document at the API root when asked for it explicitly. */
159
160
  declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<PostgrestOpenApiDoc>;
160
161
 
161
162
  /**
162
163
  * Builds the `Deno.serve`-compatible MCP handler. Everything expensive
163
- * (OpenAPI fetch, `agentify_meta()` call, doc loading/chunking) happens
164
+ * (OpenAPI fetch, `exposify_meta()` call, doc loading/chunking) happens
164
165
  * lazily on first request and is cached in-memory for the life of the edge
165
166
  * instance -- "cold start" in the spec's sense, not literally module load,
166
167
  * since env vars and the incoming JWT aren't available before the first
@@ -168,4 +169,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
168
169
  */
169
170
  declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
170
171
 
171
- export { type AgentifyManifest, type AgentifySupabaseOptions, MANIFEST_VERSION, MissingAgentifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchAgentifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
172
+ export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
package/dist/index.d.ts CHANGED
@@ -7,18 +7,18 @@ interface ColumnSchema {
7
7
  /** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
8
8
  format?: string;
9
9
  required: boolean;
10
- /** From `agentify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
10
+ /** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
11
11
  description?: string;
12
12
  isPrimaryKey?: boolean;
13
13
  }
14
14
  interface TableSchema {
15
15
  name: string;
16
16
  rlsEnabled: boolean;
17
- /** From `agentify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
17
+ /** From `exposify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
18
18
  description?: string;
19
19
  columns: ColumnSchema[];
20
20
  }
21
- /** One row of `agentify_meta()` -- the migration returns one row per column. */
21
+ /** One row of `exposify_meta()` -- the migration returns one row per column. */
22
22
  interface MetaRow {
23
23
  table_name: string;
24
24
  rls_enabled: boolean;
@@ -62,7 +62,7 @@ interface AgentifySupabaseOptions {
62
62
  anonKey?: string;
63
63
  /** Injectable for tests; defaults to global `fetch`. */
64
64
  fetchImpl?: typeof fetch;
65
- /** Bundled fallback manifest (from `agentify-supabase generate`), used when runtime introspection fails. */
65
+ /** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
66
66
  manifest?: unknown;
67
67
  rateLimit?: number;
68
68
  rateWindowMs?: number;
@@ -98,7 +98,7 @@ interface PostgrestColumnProperty {
98
98
  /**
99
99
  * Extracts column schemas for one table from a PostgREST OpenAPI document.
100
100
  * Returns `[]` for a table PostgREST never exposed (not in `definitions` --
101
- * e.g. it exists at the catalog level per `agentify_meta()` but PostgREST's
101
+ * e.g. it exists at the catalog level per `exposify_meta()` but PostgREST's
102
102
  * role lacks USAGE/SELECT grants on it), which the caller treats the same as
103
103
  * "RLS-less": never shown to the agent.
104
104
  */
@@ -113,27 +113,27 @@ declare function columnsFromOpenApi(doc: PostgrestOpenApiDoc, table: string): Co
113
113
  declare function rpcArgSchema(doc: PostgrestOpenApiDoc, fn: string): JsonSchemaObject | null;
114
114
 
115
115
  declare const MANIFEST_VERSION = 1;
116
- interface AgentifyManifest {
116
+ interface ExposifyManifest {
117
117
  version: typeof MANIFEST_VERSION;
118
118
  generatedAt: string;
119
119
  tables: TableSchema[];
120
120
  }
121
- /** Built by `npx agentify-supabase generate` against a local/linked project. */
122
- declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): AgentifyManifest;
121
+ /** Built by `npx exposify-supabase generate` against a local/linked project. */
122
+ declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): ExposifyManifest;
123
123
  /**
124
124
  * Defensive load: a manifest is bundled JSON shipped alongside the function
125
125
  * and can go stale relative to the deployed schema, or simply be malformed
126
126
  * (hand-edited, wrong version). Returns `null` -- never throws -- so the
127
127
  * degradation ladder can fall through to the next rung instead of crashing.
128
128
  */
129
- declare function loadManifest(raw: unknown): AgentifyManifest | null;
129
+ declare function loadManifest(raw: unknown): ExposifyManifest | null;
130
130
 
131
131
  /**
132
- * Merges PostgREST's OpenAPI document (column types) with `agentify_meta()`
132
+ * Merges PostgREST's OpenAPI document (column types) with `exposify_meta()`
133
133
  * rows (RLS status + comments) into the DB tool surface.
134
134
  *
135
135
  * DB exposure policy (spec, locked): RLS-gated, reads auto. A table only
136
- * appears here if `agentify_meta()` reports `rls_enabled: true` for it AND
136
+ * appears here if `exposify_meta()` reports `rls_enabled: true` for it AND
137
137
  * PostgREST actually exposes it in its OpenAPI `definitions` (the role
138
138
  * calling this has grants on it) -- a table failing either check never
139
139
  * appears, silently, exactly like a table PostgREST doesn't know about at
@@ -142,25 +142,26 @@ declare function loadManifest(raw: unknown): AgentifyManifest | null;
142
142
  */
143
143
  declare function introspectTables(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[]): TableSchema[];
144
144
 
145
- declare class MissingAgentifyMetaError extends Error {
145
+ declare class MissingExposifyMetaError extends Error {
146
146
  constructor(cause: string);
147
147
  }
148
148
  /**
149
- * Calls the `agentify_meta()` Postgres function via PostgREST's RPC
150
- * endpoint. Requires the migration from `sql/agentify_meta.sql` to have been
149
+ * Calls the `exposify_meta()` Postgres function via PostgREST's RPC
150
+ * endpoint. Requires the migration from `sql/exposify_meta.sql` to have been
151
151
  * applied; a 404 (unknown route) or PostgREST's `PGRST202` (function not
152
152
  * found in schema cache) both mean "migration missing" and are translated
153
153
  * into a self-diagnosing error (spec: "a missing `agentify_meta()` migration
154
- * produces a self-diagnosing message naming the exact fix").
154
+ * produces a self-diagnosing message naming the exact fix" -- function
155
+ * renamed to `exposify_meta()` post-spec, see the decisions journal).
155
156
  */
156
- declare function fetchAgentifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
157
+ declare function fetchExposifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
157
158
 
158
159
  /** PostgREST serves its OpenAPI document at the API root when asked for it explicitly. */
159
160
  declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<PostgrestOpenApiDoc>;
160
161
 
161
162
  /**
162
163
  * Builds the `Deno.serve`-compatible MCP handler. Everything expensive
163
- * (OpenAPI fetch, `agentify_meta()` call, doc loading/chunking) happens
164
+ * (OpenAPI fetch, `exposify_meta()` call, doc loading/chunking) happens
164
165
  * lazily on first request and is cached in-memory for the life of the edge
165
166
  * instance -- "cold start" in the spec's sense, not literally module load,
166
167
  * since env vars and the incoming JWT aren't available before the first
@@ -168,4 +169,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
168
169
  */
169
170
  declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
170
171
 
171
- export { type AgentifyManifest, type AgentifySupabaseOptions, MANIFEST_VERSION, MissingAgentifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchAgentifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
172
+ export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ function warnOnce(message) {
70
70
  if (warned.has(message)) return;
71
71
  warned.add(message);
72
72
  try {
73
- console.warn(`[agentify-supabase] ${message}`);
73
+ console.warn(`[exposify-supabase] ${message}`);
74
74
  } catch {
75
75
  }
76
76
  }
@@ -96,6 +96,7 @@ function buildPostgrestHeaders(incoming, anonKey) {
96
96
  if (value) headers.set(name, value);
97
97
  }
98
98
  if (!headers.has("apikey")) headers.set("apikey", anonKey);
99
+ if (!headers.has("authorization")) headers.set("authorization", `Bearer ${anonKey}`);
99
100
  return headers;
100
101
  }
101
102
  function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
@@ -139,31 +140,31 @@ async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
139
140
  }
140
141
 
141
142
  // src/meta.ts
142
- var MissingAgentifyMetaError = class extends Error {
143
+ var MissingExposifyMetaError = class extends Error {
143
144
  constructor(cause) {
144
145
  super(cause);
145
- this.name = "MissingAgentifyMetaError";
146
+ this.name = "MissingExposifyMetaError";
146
147
  }
147
148
  };
148
- async function fetchAgentifyMeta(postgrestUrl, headers, fetchImpl) {
149
- const res = await fetchImpl(`${postgrestUrl}/rpc/agentify_meta`, {
149
+ async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
150
+ const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
150
151
  method: "POST",
151
152
  headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
152
153
  body: "{}"
153
154
  });
154
155
  if (res.status === 404) {
155
- throw new MissingAgentifyMetaError(
156
- "agentify_meta() is not reachable at /rpc/agentify_meta (404). Run `npx agentify-supabase init` (or apply sql/agentify_meta.sql directly) and `supabase db push`, then redeploy the function."
156
+ throw new MissingExposifyMetaError(
157
+ "exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
157
158
  );
158
159
  }
159
160
  if (!res.ok) {
160
161
  const body = await res.text().catch(() => "");
161
162
  if (/PGRST202|function .* does not exist/i.test(body)) {
162
- throw new MissingAgentifyMetaError(
163
- "agentify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/agentify_meta.sql via `supabase db push`, then redeploy the function."
163
+ throw new MissingExposifyMetaError(
164
+ "exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
164
165
  );
165
166
  }
166
- throw new Error(`agentify_meta() call failed: HTTP ${res.status} ${body}`.trim());
167
+ throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
167
168
  }
168
169
  return await res.json();
169
170
  }
@@ -194,7 +195,7 @@ function columnsFromOpenApi(doc, table) {
194
195
  // ON COLUMN` into `description`, then appends its own "Note:\nThis is a
195
196
  // Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
196
197
  // so a column with no real comment ends up `undefined`, not the
197
- // boilerplate text. agentify_meta()'s copy wins when both exist anyway
198
+ // boilerplate text. exposify_meta()'s copy wins when both exist anyway
198
199
  // -- see introspect.ts's merge.
199
200
  description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
200
201
  }));
@@ -229,7 +230,7 @@ function introspectTables(openapi, metaRows) {
229
230
  if (!fromOpenApi) continue;
230
231
  columns.push({
231
232
  ...fromOpenApi,
232
- // agentify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
233
+ // exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
233
234
  // "Column comments → param descriptions" sourced from the migration).
234
235
  description: row.column_comment ?? fromOpenApi.description
235
236
  });
@@ -272,7 +273,7 @@ async function resolveTableSchemas(deps) {
272
273
  try {
273
274
  const [openapi, metaRows] = await Promise.all([
274
275
  fetchOpenApi(deps.postgrestUrl, deps.headers, deps.fetchImpl),
275
- fetchAgentifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
276
+ fetchExposifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
276
277
  ]);
277
278
  return { tables: introspectTables(openapi, metaRows), mode: "runtime", warnings, openapi };
278
279
  } catch (err) {
@@ -282,7 +283,7 @@ async function resolveTableSchemas(deps) {
282
283
  if (manifest) {
283
284
  warnings.push({
284
285
  rung: "manifest",
285
- reason: "runtime introspection failed; serving tools from the bundled agentify.manifest.json instead"
286
+ reason: "runtime introspection failed; serving tools from the bundled exposify.manifest.json instead"
286
287
  });
287
288
  return { tables: manifest.tables, mode: "manifest", warnings };
288
289
  }
@@ -572,9 +573,9 @@ ${h.chunk.text}`).join("\n\n---\n\n") : "No matching documentation found.";
572
573
  function registerDiagnosticsResource(server, warnings) {
573
574
  if (warnings.length === 0) return;
574
575
  server.registerResource(
575
- "agentify-diagnostics",
576
- "agentify://diagnostics",
577
- { title: "agentify-supabase diagnostics", mimeType: "application/json" },
576
+ "exposify-diagnostics",
577
+ "exposify://diagnostics",
578
+ { title: "exposify-supabase diagnostics", mimeType: "application/json" },
578
579
  async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(warnings, null, 2) }] })
579
580
  );
580
581
  }
@@ -629,7 +630,7 @@ function aiReady(options = {}) {
629
630
  manifest: options.manifest
630
631
  }).then((ladder) => {
631
632
  for (const w of ladder.warnings) {
632
- console.error(`[agentify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
633
+ console.error(`[exposify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
633
634
  }
634
635
  return ladder;
635
636
  });
@@ -657,7 +658,7 @@ function aiReady(options = {}) {
657
658
  return { status: res.status, bodyText: await res.text() };
658
659
  };
659
660
  return buildSupabaseMcpServer({
660
- appName: "agentify-supabase",
661
+ appName: "exposify-supabase",
661
662
  appVersion: "0.0.0",
662
663
  tables: ladder.tables,
663
664
  cfg,
@@ -675,11 +676,11 @@ function aiReady(options = {}) {
675
676
  }
676
677
  export {
677
678
  MANIFEST_VERSION,
678
- MissingAgentifyMetaError,
679
+ MissingExposifyMetaError,
679
680
  aiReady,
680
681
  buildManifest,
681
682
  columnsFromOpenApi,
682
- fetchAgentifyMeta,
683
+ fetchExposifyMeta,
683
684
  fetchOpenApi,
684
685
  introspectTables,
685
686
  loadManifest,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@exposify/supabase",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "bin": {
10
- "agentify-supabase": "./dist/cli.js"
10
+ "exposify-supabase": "./dist/cli.js"
11
11
  },
12
12
  "exports": {
13
13
  ".": {
@@ -27,7 +27,7 @@
27
27
  "prepack": "yarn build"
28
28
  },
29
29
  "dependencies": {
30
- "@exposify/core": "0.0.0",
30
+ "@exposify/core": "0.2.0",
31
31
  "@modelcontextprotocol/sdk": "^1.12.0",
32
32
  "zod": "^3.24.0"
33
33
  },
@@ -39,5 +39,14 @@
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/ayadagain/agentready.git",
46
+ "directory": "packages/supabase"
47
+ },
48
+ "homepage": "https://github.com/ayadagain/agentready/tree/main/packages/supabase#readme",
49
+ "bugs": {
50
+ "url": "https://github.com/ayadagain/agentready/issues"
42
51
  }
43
52
  }
@@ -1,6 +1,6 @@
1
- -- agentify-supabase: schema introspection function.
1
+ -- exposify-supabase: schema introspection function.
2
2
  --
3
- -- Exposes, per PostgREST's RPC convention (POST /rpc/agentify_meta), one row
3
+ -- Exposes, per PostgREST's RPC convention (POST /rpc/exposify_meta), one row
4
4
  -- per column of every ordinary table in the `public` schema: whether Row
5
5
  -- Level Security is enabled on its table, and the table/column comments an
6
6
  -- owner wrote via `COMMENT ON TABLE` / `COMMENT ON COLUMN`.
@@ -12,7 +12,15 @@
12
12
  -- necessarily SELECT from would otherwise be blocked; the function only ever
13
13
  -- returns catalog metadata, never row data, so this does not leak anything
14
14
  -- RLS would otherwise protect.
15
- create or replace function public.agentify_meta()
15
+ --
16
+ -- Renamed from agentify_meta() -- if you already applied that migration to a
17
+ -- live project, this file alone will NOT rename it there (Postgres tracks
18
+ -- migrations by filename, not by diffing function bodies). Apply this
19
+ -- instead on an already-deployed project:
20
+ -- alter function public.agentify_meta() rename to exposify_meta();
21
+ -- or drop the old one after confirming nothing else calls it:
22
+ -- drop function public.agentify_meta();
23
+ create or replace function public.exposify_meta()
16
24
  returns table (
17
25
  table_name text,
18
26
  rls_enabled boolean,
@@ -43,4 +51,4 @@ $$;
43
51
  -- PostgREST's `anon`/`authenticated` roles must be able to call this RPC;
44
52
  -- the SECURITY DEFINER body above is what makes that safe (catalog metadata
45
53
  -- only, never row data).
46
- grant execute on function public.agentify_meta() to anon, authenticated;
54
+ grant execute on function public.exposify_meta() to anon, authenticated;