Developer docs
A floating widget lets your users send feedback, an idea, or a build request; each opens a real GitHub Issue that an AI workflow triages or builds. cosculpt is the hosted intake — one endpoint, many projects.
The HTTP API (§2) and the hosted widget (§3) are live. The JS library (§4, §6) publishes to GitHub Packages as @bgold/cosculpt — see Install below. Prefer the raw HTTP API if you don't want a registry dependency.
Hosted and self-hosted intake can coexist during a migration or as a deliberate hybrid. Cosculpt does not force a single mode: the dashboard reports Dual intake when it observes both, so you can keep labels and behavior consistent across the two paths.
Install (GitHub Packages)
The library is a private package on GitHub Packages. Point the @bgold scope at it and authenticate with a GitHub token that has read:packages:
# .npmrc
@bgold:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}npm install @bgold/cosculpt
1 · Keys & capabilities
Each project (tenant) gets a key pair from the dashboard. The key type decides what it may file — capability is fixed by prefix, not configurable:
| Key | Can file | Where it lives |
|---|---|---|
csc_pk_… publishable | feedback only | client-safe — embed in a public <script> |
csc_sk_… secret | feedback · idea · build | server only — never ship to the browser or commit |
A publishable key is structurally unable to file idea/build (the endpoint 403s them), so end-user text from a browser can never auto-fire a build. The raw key is never stored — only its hash. Keys minted before the rebrand (kzp_/kzs_) still authenticate.
2 · The intake API
The universal path — works from any language or runtime. POST /api/intake with the key as a bearer token:
curl -X POST https://cosculpt.ai/api/intake \
-H "Authorization: Bearer csc_sk_…" \
-H "Content-Type: application/json" \
-d '{"kind":"idea","message":"Add a dark mode","context":{"path":"/settings"}}'Response: { ok, number?, url?, message? } — number/url point at the created issue on success; message carries the reason on failure. Kinds are limited to what the key allows.
3 · The browser widget
The drop-in tier: paste one tag (the snippet is prefilled with your publishable key on the dashboard). It mounts a floating launcher and files feedback directly.
<script src="https://cosculpt.ai/cosculpt-widget.js" data-endpoint="https://cosculpt.ai/api/intake" data-key="csc_pk_…"></script>
Point data-endpoint at your own route instead (see §6) when signed-in users should be able to file idea/build — those need a secret key, which stays on your server.
4 · Filing from your server
To file idea/build, use the secret key server-side. Store it in COSCULPT_SECRET_KEY and use the client instead of hand-rolling the POST:
import { createCosculptClient } from '@bgold/cosculpt/client';
const cosculpt = createCosculptClient(); // reads COSCULPT_SECRET_KEY (+ COSCULPT_INTAKE_URL)
const res = await cosculpt.idea('Add a dark mode', { path: '/settings' });
// res: { ok, number?, url?, message? } — never throws
// also: cosculpt.feedback(msg, ctx?), cosculpt.build(msg, ctx?)5 · feedback vs idea vs build
The kind decides what the AI reaction workflow does once the issue is filed:
| Kind | Key needed | Reaction | Touches code? |
|---|---|---|---|
feedback | publishable or secret | Triage — comments its read, then waits | No |
idea | secret | Triage — assessed & recorded; not built until greenlit | No |
build | secret | Builds now — ships to main if safe, else a QA-branch PR | Yes |
idea and build both need a secret key, but only build changes code — so gate who may file build the hardest.
6 · Templates & the proxy
For browser users to file idea/build, the browser can't hold the secret key — so your app stands up a small proxy that authenticates the user and files server-side. One config drives both halves: it enforces who may file what, and derives which widget mode (admin vs feedback) the browser renders — so they can't drift.
// app/lib/feedback.ts — the ONE config
import { createFeedbackServer } from '@bgold/cosculpt/server/feedback';
export const feedback = createFeedbackServer({
isAdmin: (req) => sessionIsAdmin(req), // the one thing only your app knows
// routing: 'always-proxy' (default) | 'dispatch'
});
// app/api/feedback/route.ts → POST = (req) => feedback.handleIntake(req)
// app/api/feedback/config/route.ts → GET = async (req) => Response.json(await feedback.browserConfig(req))// client: hand the server config to the browser client; wire it as the widget's onSubmit
import { createBrowserClient } from '@bgold/cosculpt/client/browser';
const cfg = await (await fetch('/api/feedback/config')).json();
const client = createBrowserClient(cfg);
// <FeedbackWidget isAdmin={client.canFileAdmin} onSubmit={(i) => client.file(i)} … />Routing
- always-proxy (default, safest): every kind posts to your proxy; the browser holds no key.
- dispatch:
feedbackgoes direct to cosculpt with the publishable key;idea/buildroute through your proxy.
The file filer is injectable, so the same template works whether you file over HTTP (default, with a secret key) or another way. cosculpt itself dogfoods these templates.
7 · Environment variables
| Var | Holds | Default |
|---|---|---|
COSCULPT_SECRET_KEY | your server-side intake key (csc_sk_…) | — |
COSCULPT_INTAKE_URL | intake endpoint override | https://cosculpt.ai/api/intake |
The publishable key isn't an env var — it's embedded in the widget snippet (client-safe). Never put a secret key in browser code or the repo.