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 (§3.1) and the hosted widget (§3.2) are live. The JS library (§3.3, §5) publishes to GitHub Packages as @bgold/cosculpt — see Install (§1.1). 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.

1 · Getting started

This is how to wire the hosted loop onto a repo — separate from installing the npm library below, which is only needed if you use the JS server client or templates.

  1. Sign in: Sign in with GitHub. A fresh account is sent straight to installing the App on a repo.
  2. Install the App: install the Cosculpt GitHub App on the repo you want feedback wired into — it posts issues as a bot, no personal token. You can add more repos later from the dashboard.
  3. Add the workflow: on your dashboard, click Add the workflow — it opens a PR adding the AI reaction workflow (triages feedback, builds anything labeled build) to the repo. Pick an engine (Claude or Codex) and how it's billed (a metered API key, or your existing subscription).
  4. Set the secret: set that engine's secret (e.g. ANTHROPIC_API_KEY) in the repo's Settings → Secrets — the dashboard names the exact secret once the workflow PR is open.
  5. Grab your keys: grab your project's keys from the dashboard: drop the publishable key into the widget snippet, or use the secret key from your server.

1.1 · 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

2 · 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.

3 · Posting feedback

Three ways to file, from the same universal endpoint to a drop-in browser tag to a server client.

3.1 · 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.dev/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.2 · The browser widget

The same widget ships in three tiers — pick the one that matches your stack. All three render the same floating launcher and share the same options; only the mounting mechanics differ.

No bundler — the drop-in tag. 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.dev/cosculpt-widget.js"
  data-endpoint="https://cosculpt.dev/api/intake"
  data-key="csc_pk_…"></script>

Point data-endpoint at your own route instead (see §5) when signed-in users should be able to file idea/build — those need a secret key, which stays on your server.

Reactimport { CosculptWidget } from '@bgold/cosculpt/widget', a component that takes app couplings (pathname, onSubmit, labelForPath) as props. This is the variant §5's templates use, and the only one with an approvals panel.

Vanilla JS / non-React bundler import { mountCosculptWidget } from '@bgold/cosculpt/widget/vanilla', the same widget for esbuild/rollup/etc. apps with no React dependency; couplings are options instead of props. It's also what the drop-in <script> tag above bundles under the hood.

3.3 · 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?)

Environment variables

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

4 · 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.

5 · 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 (sculpt vs feedback) the browser renders — so they can't drift.

// app/lib/feedback.ts — the ONE config (this file lives in YOUR app; name it whatever you like)
import { createFeedbackServer } from '@bgold/cosculpt/server/feedback';
export const feedback = createFeedbackServer({
  isSculptor: (req) => sessionIsSculptor(req),   // the one thing only your app knows
  // routing: 'always-proxy' (default) | 'dispatch'
});
// app/api/feedback/route.ts — imports the config above, wires the proxy
import { feedback } from '../../lib/feedback';
export const POST = (req: Request) => feedback.handleIntake(req);

// app/api/feedback/config/route.ts — same import, one level deeper
import { feedback } from '../../../lib/feedback';
export const GET = async (req: Request) => Response.json(await feedback.browserConfig(req));
// client: hand the server config to the browser client, then pass it straight to the widget
import { createBrowserClient } from '@bgold/cosculpt/client/browser';
const cfg = await (await fetch('/api/feedback/config')).json();
const client = createBrowserClient(cfg);
// <CosculptWidget client={client} pathname={pathname} … />
// (client={client} is shorthand for isSculptor={client.canSculpt} onSubmit={(i) => client.file(i)} — pass
// isSculptor/onSubmit yourself only if you need something other than the browser client's default wiring.)

Why fetch instead of importing feedback straight into the client: it's a server module (closes over your isSculptor check), and mode is derived per request — a build-time import runs once with no request to derive it from, so each visitor's browser has to ask the server for its own answer.

Routing

  • always-proxy (default, safest): every kind posts to your proxy; the browser holds no key.
  • dispatch: feedback goes direct to cosculpt with the publishable key; idea/build route 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.

6 · Self-hosting (no cosculpt.dev dependency)

Everything above assumes the hosted intake — your widget posts to https://cosculpt.dev/api/intake, and cosculpt's service files the GitHub issue on your behalf using its own App key. If you'd rather nothing about a user's feedback ever transits cosculpt's servers — trust, privacy, air-gapped, or org-policy reasons — you can self-host intake instead: your own server files the issue directly, with your own GitHub App credentials. Same widget, same issue shape, same reaction workflow; only who holds the credential and where the POST lands differ.

Concretely: instead of a COSCULPT_SECRET_KEY, your server holds a GitHub App's GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY / GITHUB_APP_INSTALLATION_ID (or a plain GITHUB_ISSUE_TOKEN PAT), and calls the library's intake function directly — no network request to cosculpt.dev at all:

import { createCosculptIntake } from '@bgold/cosculpt';
const intake = createCosculptIntake({ repo: 'you/your-repo' }); // reads GITHUB_APP_* from env
const res = await intake({ kind: 'idea', message: 'Add a dark mode', context: { path: '/settings' } });

intake has the same shape as the file option from §5's templates — hand it straight to createFeedbackServer({ isSculptor, file: intake }) and the proxy/widget/authz wiring is unchanged; only the filer swaps from an HTTP call into cosculpt to a direct GitHub App call from your own process. A worked example (byte-identical front end, only the server differs) lives in the repo at examples/sites/self-hosted/, alongside the hosted equivalent in examples/sites/hosted/ — a test proves the two paths file the identical issue.

Self-hosting intake and using the dashboard are independent — you can still connect the dashboard for Add the workflow, Health check, and the self-healing sweep, since those only need the GitHub App installed on your repo, not intake to be hosted. The dashboard will show the repo as Self-hosted intake once it notices issues arriving without cosculpt's provenance marker. The trade-off is yours to hold: you mint and rotate the GitHub App private key yourself, in exchange for feedback content never reaching cosculpt's servers. See docs/github-app-setup.md in the repo for creating the App and the exact env vars.

Dashboard · Health at /api/health · Sign in