Supabase guard: stops Supabase calls aimed at a project other than the current folder's, and asks in a one-key popup before destructive SQL, branch resets or…

Saját Claude Code mod-ok (function hook pluginek) egy pluginpiacon.
claude plugin marketplace add Szotasz/claude-mods
claude plugin install usage-band@claude-mods
claude plugin install usage-header@claude-mods
claude plugin install project-pane@claude-mods
claude plugin install focus-mode@claude-mods
claude plugin install tool-hub@claude-mods
claude plugin install snake-pane@claude-mods
claude plugin install supabase-guard@claude-mods
claude plugin install public-repo-guard@claude-mods
claude plugin install decision-popup@claude-mods
Frissítés: claude plugin update usage-band@claude-mods, majd /reload-plugins (vagy új session).
A státuszsorba (a prompt alá) írja:
🤖 Opus 5.5 │ Ctx ███░░░░░ 42% 420k/1M │ 5h ███░░░░░ 34% → 16:20 │ Hét ███████░ 93% ⚠ → Cs 10:00
/model váltáskor és turn végén frissül)Nyelv / Language: /config → usage-band.language, vagy a settings.json-ban:
"pluginConfigs": { "usage-band": { "language": "en" } }
auto (alapértelmezett): magyar, ha a rendszer nyelve (LANG) magyar, különben angolhu: magyar (Hét, Cs 10:00)en: English (Week, Thu 10:00)Követelmény: Claude Code 2.1.289 körüli verzió. A mod-API early access, kiadásról kiadásra változhat.
Sáv a prompt fölött: a kontextusablak kategóriánként színezve, alatta az 5 órás és a heti limit.
Kontextus ██▓▓▓▓▓▓████████████▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░ 42% 84k/200k
■ Rendszerprompt 3k ■ Eszközök 12k ■ Memória 400 ■ Üzenetek 69k ▒ Tömörítési tartalék 33k ░ Szabad 83k
Limitek 5 óra ███████░░░░░░░░░░░░░ 34% → 16:20 Hét ███████████████████░ 93% ⚠ → Cs 10:00
/context saját színével látszik (rendszerprompt, eszközök, MCP, memóriafájlok, skillek, ágensek, üzenetek), utána a tömörítési tartalék és a szabad hely halványan; alatta jelmagyarázat tokenszámmal. Az igény szerint betöltött (halasztott) eszközsémák kimaradnak, mert nincsenek az ablakban.$.session.usage({ breakdown: 'summary' })), API-hívás nélkül; minden turn végén és limitmozduláskor frissül./hasznalat kapcsolja, /hasznalat be|ki beállítja; az állapotot sessionök között megőrzi. Az eszköztárban (/eszkozok) is kapcsolható.Beállítás (/config): usage-header.language (auto/hu/en).
Oldalpanel az aktuális git repóhoz. Magától megnyílik, ha a session git repóban indul és a terminál legalább 144 oszlop széles (miután egyszer /project-tel megnyitottad, 110 is elég); keskenyebb ablaknál egy toast szól, hogy vár. Bármikor: /project.
acme/shop ↻ most [ Frissítés ]
GIT
🌿 develop ↑0 ↓0 · 7 módosított fájl
utolsó commit: 57704fe · 7 hónapja — Fix checkout flow
⚠ a main 13 committal előrébb jár, te a develop branch-en vagy
Előbb commitold a módosításokat, vagy tedd félre őket:
[ Stash + szinkron ]
DEPLOY (Netlify)
✔ ready main 3 perce
PULL REQUESTEK
✔ #142 Stripe webhook retry · approved
CI (GitHub Actions)
✘ netlify-deploy-verify main · 5 napja [ Kérdezd Claude-ot ]
SUPABASE
abcdefghijklmnopqrst · Acme Shop · eu-west-3
18 migráció · utolsó: 018_add_orders_rls.sql
GitHub Netlify Supabase
s): csak fast-forward, soha nem merge-öl, rebase-el vagy töröl.git pull --ff-onlygit switch main + git merge --ff-only origin/maingit stash push -u, vissza: git stash popr): 60 mp-enként és minden turn végén; git fetch 5 percenként.a): a legutóbbi elbukott CI-futásról kérdést tesz a promptba.Adatforrások (a CLI-k saját bejelentkezésével, a plugin tokent nem olvas):
| Szekció | Forrás | Ha hiányzik |
|---|---|---|
| Git | git | — |
| PR, CI | gh (gh auth login) | tipp a panelen |
| Deploy | netlify CLI + .netlify/state.json | npm i -g netlify-cli && netlify login |
| Supabase | supabase/.temp/project-ref, supabase/migrations, supabase projects list | supabase login az állapothoz |
Beállítás (/config): project-pane.language (auto/hu/en), project-pane.autoOpen (alapból be).
Fókusz mód: a transcriptből eltűnnek az eszközhívások és eredményeik, a köztes szövegek, a saját promptjaid és a parancsok kimenete. Csak két dolog marad: a prompt fölötti sávban a részfeladatok listája állapotsávval és pipával, alatta a turn végső válasza.
✔ Meglévő mod-repo áttekintése
◐ Játék-mod megírása ██████░░░░ 60%
○ Felvétel az eszközök közé
/fokusz kapcsolja, /fokusz be / /fokusz ki beállítja; az állapotot sessionök között megőrzi.mcp__focus-mode__plan eszközzel; a rendszerprompt bekapcsolt állapotban erre utasítja (minden több lépéses kérésnél előbb a részfeladatok, aztán frissítés indításkor, haladáskor és befejezéskor).Beállítás (/config): focus-mode.language (auto/hu/en).
Eszköztár: egy helyről kapcsolhatók a fenti modok. A prompt alatti lábléc jobb oldalán lévő ⚙ Eszközök gomb (vagy /eszkozok) a prompt fölötti sávban nyitja meg:
⚙ Szabolcs eszközei kattintás, vagy ctrl+x tab után szám [ Bezár ]
1. Fókusz mód ● BE [ Kikapcsol ] · Csak a részfeladatok listája és a végső válasz látszik
2. Limitsáv ○ KI [ Bekapcsol ] · Alsó sáv: 5 órás és heti limit, kontextusablak
3. Használati sáv ● BE [ Kikapcsol ] · Prompt fölött: kontextus kategóriánként színezve, 5 órás és heti limit
4. Projektpanel ● BE [ Kikapcsol ] · GitHub, CI, Netlify és Supabase állapot oldalt
5. Kígyó játék ● BE [ Kikapcsol ] · Oldalt nyílik promptküldéskor, amíg Claude dolgozik
ctrl+x tab után az 1–5 számbillentyűvel is./fokusz, /limitsav, /hasznalat, /project, /jatek (mind be|ki argumentummal).Új mod felvétele az eszköztárba: a mod írja a saját isOn állapotát, adjon /<parancs> be|ki-t, és hookolja a state.set-et { plugin: 'tool-hub', key: 'request' }-re; a hubban egy sor a TOOLS listába, egy ág a stateOf-ba és a kulcs a types/index.d.ts-be.
Beállítás (/config): tool-hub.language (auto/hu/en).
Kígyó játék az oldalpanelben, amíg Claude dolgozik.
⏳ Claude dolgozik…
Pont: 4 Rekord: 17
╭──────────────────────────────────────╮
│ │
│ ████████ │
│ ██ ● │
╰──────────────────────────────────────╯
p: szünet · r: újra
w: ↑ a: ← s: ↓ d: → p: szünet r: új
/jatek mindig nyitja). /jatek ki kikapcsolja, az eszköztárban (/eszkozok) is kapcsolható.wasd / hjkl, szóköz vagy p szünet, r új játék. Vagy ctrl+x tab a panelre, és a w a s d p r gombok.Client surface modul), nem terheli a sessiont.Supabase-őr: megállítja a Supabase-hívást, ha nem az aktuális mappa projektjére menne, és egygombos felugróban rákérdez.
supabase/.temp/project-ref, a .env* fájlok https://<ref>.supabase.co címei és a CLAUDE.md (ha egyetlen projekt-ID-t említ) adják, plusz amit a felugróban „Ez a mappa projektje” gombbal megerősítettél.execute_sql, apply_migration, deploy_edge_function, branch-műveletek, illetve supabase db push --project-ref …): kérdez — Megtiltom / Engedélyezem / Ez a mappa projektje.DROP, TRUNCATE, DELETE FROM, WHERE nélküli UPDATE, REVOKE, RLS kikapcsolása, reset_branch, delete_branch, pause_project, supabase db reset --linked./supaor be|ki, vagy az eszköztárban.Push-őr: nyilvános GitHub-repóba push előtt (és gh repo edit --visibility public / gh repo create --public előtt) átnézi, mi menne ki.
git push origin forrás:cél alakot is.project_id, táblázatsor), JWT / Supabase-kulcs, Stripe/Anthropic/OpenAI/GitHub/AWS/Google/Slack/Resend/Telegram/ElevenLabs kulcsok, privát kulcs, e-mail-cím (a saját git e-mail és a noreply/példa címek kivételével), helyi /Users/<te>/ útvonal, privát repóid nevei (gh repo list --visibility private), .env fájlok, és amit /pushor figyel <szöveg>-gel felvettél.gh CLI (bejelentkezve). /pushor be|ki.Döntési felugró: a döntések egygombos kérdésként jönnek.
/dontes be|ki, vagy az eszköztárban.project-pane bővítése:
vercel ls)Státuszsor bővítése (usage-band):
/compact$.session.usage().cost)userConfig)Új mod-ok:
.env, kulcsfájlok, supabase/.temp szerkesztésének tiltása (tool.call → deny)turn.complete, $.audio.play)/quote — a kijelölt szöveget idézetként a promptba teszi ($.ui.selection)prompt.compose)Egy plugin mappája közvetlenül is betölthető:
claude --plugin-dir ./plugins/usage-band
Ellenőrzés:
claude plugin validate plugins/usage-band
claude plugin test plugins/usage-bandhooks/register.tsx 243 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { LOOK_IN, actionRisk, isRef, refsIn, short, supabaseAction, supabaseCli } from './classify'
5import type { Risk } from './classify'
6
7const isOn = atom({ plugin: 'supabase-guard', key: 'isOn' } as const, true)
8
9type Lang = 'hu' | 'en'
10
11const STRINGS = {
12 hu: {
13 header: 'Supabase-őr',
14 command: 'Supabase-őr be/ki: rossz projektre menő és romboló Supabase-hívások megállítása',
15 on: 'Supabase-őr bekapcsolva.',
16 off: 'Supabase-őr kikapcsolva: a Supabase-hívások ellenőrzés nélkül futnak.',
17 usage: 'Használat: /supaor [be|ki]',
18 deny: 'Megtiltom',
19 once: 'Engedélyezem',
20 session: 'Engedem a session végéig',
21 bind: 'Ez a mappa projektje',
22 mismatch: (what: string, target: string, folder: string, own: string) =>
23 `${what} a(z) ${target} projekten — de ez a mappa (${folder}) a(z) ${own} projekthez tartozik. Engeded?`,
24 unknown: (what: string, target: string, folder: string) =>
25 `${what} a(z) ${target} projekten. Ehhez a mappához (${folder}) nem találtam Supabase-projektet. Engeded?`,
26 destructive: (what: string, target: string) => `Romboló művelet a(z) ${target} projekten: ${what}. Engeded?`,
27 readNote: (target: string, own: string) =>
28 `Figyelem (supabase-guard): ez a hívás a(z) ${target} projektet olvasta, de az aktuális mappa projektje ${own}. Ellenőrizd, hogy a jó projektről vonsz-e le következtetést.`,
29 denied: (what: string) =>
30 `A supabase-guard megállította (${what}): a felhasználó nem engedélyezte. Ne próbáld meg más úton; kérdezd meg, mit szeretne, vagy használd a mappához tartozó projektet.`,
31 dismissed: (what: string) => `A supabase-guard megállította (${what}): a felhasználó bezárta a kérdést. Ne futtasd újra kérdés nélkül.`,
32 other: (answer: string) => `A supabase-guard megállította; a felhasználó válasza: „${answer}”`,
33 bound: (folder: string, ref: string) => `Megjegyeztem: a(z) ${folder} mappa projektje ${short(ref)}.`,
34 },
35 en: {
36 header: 'Supabase',
37 command: 'Supabase guard on/off: stop Supabase calls to the wrong project and destructive ones',
38 on: 'Supabase guard on.',
39 off: 'Supabase guard off: Supabase calls run unchecked.',
40 usage: 'Usage: /supaor [on|off]',
41 deny: 'Deny',
42 once: 'Allow once',
43 session: 'Allow for this session',
44 bind: "It is this folder's project",
45 mismatch: (what: string, target: string, folder: string, own: string) =>
46 `${what} on project ${target}, but this folder (${folder}) belongs to ${own}. Allow it?`,
47 unknown: (what: string, target: string, folder: string) =>
48 `${what} on project ${target}. I found no Supabase project for this folder (${folder}). Allow it?`,
49 destructive: (what: string, target: string) => `Destructive action on project ${target}: ${what}. Allow it?`,
50 readNote: (target: string, own: string) =>
51 `Note (supabase-guard): this call read project ${target}, but the current folder's project is ${own}. Make sure you draw conclusions about the right project.`,
52 denied: (what: string) =>
53 `Stopped by supabase-guard (${what}): the user did not allow it. Do not try another route; ask them what they want, or use this folder's project.`,
54 dismissed: (what: string) => `Stopped by supabase-guard (${what}): the user closed the question. Do not run it again without asking.`,
55 other: (answer: string) => `Stopped by supabase-guard; the user answered: "${answer}"`,
56 bound: (folder: string, ref: string) => `Noted: the project of ${folder} is ${short(ref)}.`,
57 },
58}
59
60let lang: Lang = 'en'
61const t = () => STRINGS[lang]
62
63async function pickLanguage($: EngineInterface, setting: unknown): Promise<Lang> {
64 if (setting === 'hu' || setting === 'en') return setting
65 const locale = (await $.env.get('LC_ALL')) || (await $.env.get('LANG')) || ''
66 return locale.toLowerCase().startsWith('hu') ? 'hu' : 'en'
67}
68
69const basename = (path: string) => path.replace(/\/+$/, '').split('/').pop() || path
70
71// Session szintű engedélyek (`ref|kockázat`): újratöltéskor elfelejtődnek, ez szándékos.
72const allowed = new Set<string>()
73// Mappánkénti gyorsítótár, hogy ne olvassuk minden hívásnál a fájlokat.
74const cache = new Map<string, { at: number; refs: string[] }>()
75const CACHE_MS = 60_000
76
77type Bindings = Record<string, string[]>
78type Names = Record<string, string>
79
80async function storeObject<T extends object>($: EngineInterface, key: string): Promise<T> {
81 const value = await $.store.get(key)
82 return (typeof value === 'object' && value !== null ? value : {}) as T
83}
84
85// A mappa projektje: a supabase CLI link, a .env fájlok URL-jei, a CLAUDE.md (ha egyetlen ref-et említ), és amit a felhasználó megerősített.
86async function expected($: EngineInterface): Promise<{ root: string; refs: string[] }> {
87 const cwd = await $.session.cwd()
88 const root = (await $.session.repo())?.root ?? cwd
89 const hit = cache.get(root)
90 if (hit !== undefined && Date.now() - hit.at < CACHE_MS) return { root, refs: hit.refs }
91
92 const refs = new Set<string>((await storeObject<Bindings>($, 'bindings'))[root] ?? [])
93 for (const dir of new Set([root, cwd])) {
94 for (const file of LOOK_IN) {
95 const path = `${dir}/${file}`
96 if (!(await $.fs.exists(path))) continue
97 const found = refsIn(await $.fs.read(path))
98 if (file === 'CLAUDE.md' && found.length !== 1) continue
99 for (const ref of found) refs.add(ref)
100 }
101 }
102 const list = [...refs]
103 cache.set(root, { at: Date.now(), refs: list })
104
105 // Megjegyezzük, melyik ref melyik mappáé, hogy máshol is néven nevezhessük.
106 if (list.length > 0) {
107 const names = await storeObject<Names>($, 'names')
108 const folder = basename(root)
109 if (list.some(ref => names[ref] !== folder)) {
110 await $.store.set('names', { ...names, ...Object.fromEntries(list.map(ref => [ref, folder])) })
111 }
112 }
113 return { root, refs: list }
114}
115
116async function label($: EngineInterface, ref: string): Promise<string> {
117 const name = (await storeObject<Names>($, 'names'))[ref]
118 return name === undefined ? ref : `${name} (${short(ref)})`
119}
120
121type Verdict = { allow: true; note?: string } | { allow: false; reason: string }
122
123async function judge($: EngineInterface, ref: string | null, risk: Risk, what: string): Promise<Verdict> {
124 if (!(await read($, isOn))) return { allow: true }
125 const { root, refs } = await expected($)
126 const folder = basename(root)
127 const known = refs.length > 0
128 const mismatch = known && ref !== null && !refs.includes(ref)
129
130 if (risk === 'read') {
131 if (!mismatch || ref === null) return { allow: true }
132 const own = (await Promise.all(refs.map(r => label($, r)))).join(', ')
133 return { allow: true, note: t().readNote(await label($, ref), own) }
134 }
135
136 const key = `${ref ?? '?'}|${risk}`
137 if (allowed.has(key)) return { allow: true }
138 const target = ref === null ? '?' : await label($, ref)
139
140 let question: string
141 let options: string[]
142 if (mismatch) {
143 const own = (await Promise.all(refs.map(r => label($, r)))).join(', ')
144 question = t().mismatch(what, target, folder, own)
145 options = risk === 'destructive' ? [t().deny, t().once] : [t().deny, t().once, t().bind]
146 } else if (risk === 'destructive') {
147 question = t().destructive(what, target)
148 options = [t().deny, t().once]
149 } else if (!known && ref !== null) {
150 question = t().unknown(what, target, folder)
151 options = [t().deny, t().once, t().session, t().bind]
152 } else {
153 return { allow: true }
154 }
155
156 let answer: string
157 try {
158 answer = await $.ui.ask(question, { header: t().header, options })
159 } catch {
160 return { allow: false, reason: t().dismissed(what) }
161 }
162 if (answer === t().once) return { allow: true }
163 if (answer === t().session) {
164 allowed.add(key)
165 return { allow: true }
166 }
167 if (answer === t().bind && ref !== null) {
168 const bindings = await storeObject<Bindings>($, 'bindings')
169 await $.store.set('bindings', { ...bindings, [root]: [...new Set([...(bindings[root] ?? []), ref])] })
170 cache.delete(root)
171 $.ui.toast(t().bound(folder, ref))
172 return { allow: true }
173 }
174 if (answer === t().deny) return { allow: false, reason: t().denied(what) }
175 return { allow: false, reason: t().other(answer) }
176}
177
178function describe(action: string, input: Record<string, unknown>): string {
179 const sql = typeof input.query === 'string' ? input.query.replace(/\s+/g, ' ').trim() : ''
180 const name = typeof input.name === 'string' ? ` ${input.name}` : ''
181 if (sql === '') return `${action}${name}`
182 return `${action}${name}: ${sql.length > 120 ? `${sql.slice(0, 119)}…` : sql}`
183}
184
185async function setOn($: EngineInterface, value: boolean): Promise<void> {
186 await update($, isOn, () => value)
187 await $.store.set('isOn', value)
188}
189
190export const register: Register = (on, options) => {
191 if (options.language === 'hu' || options.language === 'en') lang = options.language
192
193 on('session.start', async ($, e, next) => {
194 lang = await pickLanguage($, options.language)
195 const saved = await $.store.get('isOn')
196 await update($, isOn, () => saved !== false)
197 await $.command.register({
198 name: 'supaor',
199 description: t().command,
200 argumentHint: lang === 'hu' ? '[be|ki]' : '[on|off]',
201 immediate: true,
202 })
203 return next(e)
204 })
205
206 on('command.run', { command: 'supaor' }, async ($, e) => {
207 const arg = e.args.trim().toLowerCase()
208 const wanted = ['be', 'on'].includes(arg) ? true : ['ki', 'off'].includes(arg) ? false : arg === '' ? !(await read($, isOn)) : null
209 if (wanted === null) return { text: t().usage }
210 await setOn($, wanted)
211 return { text: wanted ? t().on : t().off }
212 })
213
214 on('state.set', { plugin: 'tool-hub', key: 'request' }, async ($, e, next) => {
215 const done = await next(e)
216 if (e.value?.tool === 'supabase') await setOn($, e.value.on)
217 return done
218 })
219
220 // Supabase MCP eszközök (bármelyik szerveren, aminek a nevében szerepel a „supabase”).
221 on('tool.call', async ($, e, next) => {
222 const action = supabaseAction(e.tool)
223 if (action === null) return next(e)
224 const input = e as unknown as Record<string, unknown>
225 const ref = isRef(input.project_id) ? input.project_id : null
226 const what = describe(action, input)
227 const verdict = await judge($, ref, actionRisk(action, input), what)
228 if (!verdict.allow) return { deny: verdict.reason }
229 const result = await next(e)
230 if (verdict.note === undefined || result.deny !== undefined) return result
231 return { ...result, context: [...(result.context ?? []), verdict.note] }
232 }).catch(($, e, next) => (next.called || supabaseAction(e.tool) === null ? next(e) : { deny: 'supabase-guard: the check failed, so the call was stopped.' }))
233
234 // A supabase CLI a Bash-ban.
235 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
236 const cli = supabaseCli(e.command)
237 if (cli === null || cli.risk === 'read') return next(e)
238 const verdict = await judge($, cli.ref, cli.risk, `supabase ${cli.what}`)
239 if (!verdict.allow) return { deny: verdict.reason }
240 return next(e)
241 }).catch(($, e, next) => (next.called || supabaseCli(e.command) === null ? next(e) : { deny: 'supabase-guard: the check failed, so the command was stopped.' }))
242}
243hooks/classify.ts 99 lines1// Tiszta függvények: melyik Supabase-hívás milyen veszélyes, és melyik projekthez tartozik a mappa.
2
3export type Risk = 'read' | 'write' | 'destructive'
4
5// Projekt-ref: a Supabase 20 kisbetűs azonosítója.
6const REF = /^[a-z]{20}$/
7
8export const isRef = (value: unknown): value is string => typeof value === 'string' && REF.test(value)
9
10// mcp__supabase__execute_sql, mcp__claude_ai_Supabase__execute_sql, … → execute_sql
11export function supabaseAction(tool: string): string | null {
12 const m = /^mcp__([^_].*?)__(.+)$/.exec(tool)
13 if (m === null || !/supabase/i.test(m[1] ?? '')) return null
14 return m[2] ?? null
15}
16
17const READ_ACTIONS = /^(list_|get_|search_docs$|generate_typescript_types$|query_logs$|confirm_cost$)/
18// Ezek a megegyező projekten is mindig kérdeznek.
19const DESTRUCTIVE_ACTIONS = new Set(['delete_branch', 'reset_branch', 'pause_project', 'restore_project'])
20
21// DROP, TRUNCATE, DELETE, ALTER … DROP, jogosultság-visszavonás, WHERE nélküli UPDATE.
22const DESTRUCTIVE_SQL = [
23 /\bdrop\s+(table|schema|database|function|view|materialized|type|trigger|policy|index|extension|role|publication)\b/i,
24 /\btruncate\b/i,
25 /\bdelete\s+from\b/i,
26 /\balter\s+table\b[^;]*\bdrop\b/i,
27 /\brevoke\b/i,
28 /\bdisable\s+row\s+level\s+security\b/i,
29]
30const READ_SQL = /^\s*(select|with|explain|show|values|table)\b/i
31
32function stripSqlComments(sql: string): string {
33 return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
34}
35
36function updateWithoutWhere(sql: string): boolean {
37 return stripSqlComments(sql)
38 .split(';')
39 .some(stmt => /^\s*update\b/i.test(stmt) && !/\bwhere\b/i.test(stmt))
40}
41
42export function sqlRisk(sql: string): Risk {
43 const clean = stripSqlComments(sql)
44 if (DESTRUCTIVE_SQL.some(re => re.test(clean)) || updateWithoutWhere(clean)) return 'destructive'
45 const statements = clean.split(';').filter(s => s.trim() !== '')
46 if (statements.length > 0 && statements.every(s => READ_SQL.test(s) && !/\b(insert|update|delete)\b/i.test(s))) return 'read'
47 return 'write'
48}
49
50export function actionRisk(action: string, input: Record<string, unknown>): Risk {
51 if (action === 'execute_sql') return sqlRisk(typeof input.query === 'string' ? input.query : '')
52 if (DESTRUCTIVE_ACTIONS.has(action)) return 'destructive'
53 if (action === 'apply_migration' && typeof input.query === 'string' && sqlRisk(input.query) === 'destructive') return 'destructive'
54 if (READ_ACTIONS.test(action)) return 'read'
55 return 'write'
56}
57
58// A Bash-ban futó supabase CLI: --project-ref és a veszélyes alparancsok.
59export type CliCall = { ref: string | null; risk: Risk; what: string }
60
61export function supabaseCli(command: string): CliCall | null {
62 if (!/(^|[\s;&|(])(npx\s+|bunx\s+)?supabase\s/.test(command)) return null
63 const ref = /--project-ref[=\s]+["']?([a-z]{20})\b/.exec(command)?.[1] ?? null
64 const what = /supabase\s+((?:[a-z][a-z-]*\s*){1,2})/.exec(command)?.[1]?.trim() ?? 'supabase'
65 let risk: Risk = 'read'
66 if (/\bdb\s+reset\b[^;&|]*--linked|\bprojects\s+delete\b|\bbranches\s+delete\b|\bmigration\s+repair\b/.test(command)) risk = 'destructive'
67 else if (/\bdb\s+push\b|\bfunctions\s+deploy\b|\bsecrets\s+(set|unset)\b|\bfunctions\s+delete\b|\bconfig\s+push\b/.test(command)) risk = 'write'
68 return { ref, risk, what }
69}
70
71// A mappa fájljaiból kiolvasható projekt-ref-ek.
72export const LOOK_IN = [
73 'supabase/.temp/project-ref',
74 '.env',
75 '.env.local',
76 '.env.development',
77 '.env.development.local',
78 '.env.production',
79 'frontend/.env',
80 'frontend/.env.local',
81 'web/.env.local',
82 'app/.env.local',
83 'CLAUDE.md',
84]
85
86export function refsIn(text: string): string[] {
87 const found = new Set<string>()
88 const trimmed = text.trim()
89 if (isRef(trimmed)) found.add(trimmed)
90 for (const m of text.matchAll(/https?:\/\/([a-z]{20})\.supabase\.(co|in)\b/g)) found.add(m[1] ?? '')
91 for (const m of text.matchAll(/project[ _-]?(?:id|ref)\W{1,6}([a-z]{20})\b/gi)) found.add(m[1] ?? '')
92 for (const m of text.matchAll(/SUPABASE_(?:PROJECT_)?(?:ID|REF)\s*=\s*["']?([a-z]{20})\b/g)) found.add(m[1] ?? '')
93 return [...found].filter(isRef)
94}
95
96export function short(ref: string): string {
97 return `${ref.slice(0, 6)}…`
98}
99types/index.d.ts 10 lines1// Az eszköztár (tool-hub) kérése: a kapcsoló gomb ezt írja, a mod a state.set hookjában veszi át.
2export type HubRequest = { tool: string; on: boolean; n: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'tool-hub': { request: HubRequest | null }
7 'supabase-guard': { isOn: boolean }
8 }
9}
10