← cosculpt

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:

KeyCan fileWhere it lives
csc_pk_… publishablefeedback onlyclient-safe — embed in a public <script>
csc_sk_… secretfeedback · idea · buildserver 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:

KindKey neededReactionTouches code?
feedbackpublishable or secretTriage — comments its read, then waitsNo
ideasecretTriage — assessed & recorded; not built until greenlitNo
buildsecretBuilds now — ships to main if safe, else a QA-branch PRYes

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

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

VarHoldsDefault
COSCULPT_SECRET_KEYyour server-side intake key (csc_sk_…)
COSCULPT_INTAKE_URLintake endpoint overridehttps://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.

Dashboard · Health at /api/health · Sign in