Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Model-agnostic LLM selection.** `--model <id>` accepts any Anthropic or
OpenAI model string; `--provider` takes vendors (`anthropic|openai`) or
shortcuts. The same choice applies to soak and steep. Web BYOK panel offers
either vendor plus a free-form model field.
- `GET /api/health` for uptime checks and the OpenAPI catalog.
- `sink demo` documented in the README command table.

### Fixed

- Vercel git deploys were building the repo root and looking for `public/`,
ignoring `web/vercel.json`. Added a root `vercel.json` that builds the web
app to `web/dist` and serves edge functions from root `api/`.
- CI install failed under pnpm 11 supply-chain checks because the SheetJS CDN
`xlsx` tarball had no lockfile integrity hash. Integrity is now recorded.
- `pnpm-workspace.yaml` left `core-js` as an unresolved `allowBuilds` placeholder
(`set this to true or false`), which also fails pnpm 11 installs. Explicitly
denied.
- Agent discovery docs claimed OAuth, MCP HTTP, and registration endpoints that
do not exist. Discovery files now match the real surface: CLI, browser demo,
`/api/health`, and `/api/firecrawl-proxy`.
- README pointed at the Vercel preview hostname and an outdated org path; primary
demo link is datasink.dev and GitHub links use `chrisschouk/sink-cli`.
- README provider flags and tagline brought in line with the CLI (`haiku|sonnet|…`,
four-phase strapline).

## [0.4.0] - 2026-06-11

### Added
Expand Down
13 changes: 9 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ for library consumers.

## Publish flow (npm)

Published as `datasink` (no scope). Current version 0.4.0 is not yet published to npm
(the web demo was the focus; CLI behaviour is unchanged from 0.3.x).
Published as `datasink` (no scope). Current version on npm is **0.4.0** (published 2026-06-11).

**2FA gotcha**: interactive `npm publish` hits the 2FA wall in non-interactive environments.
Use a granular **automation token** (npm account → Access Tokens → Generate New Token →
Expand All @@ -46,8 +45,14 @@ Check the token type before any publish attempt.

## Web demo

`web/` is a Vite + React app. Deploy: `cd web && vercel build --prod && vercel deploy --prebuilt --prod`
(auto-deploy requires Root Directory = `web` in the Vercel `sink-web` project settings — see NEXT_SESSION.md).
`web/` is a Vite + React app. Deployed as the Vercel `sink-web` project (domain datasink.dev).

Git-linked deploys use the **repo-root** [`vercel.json`](vercel.json): build datasink +
`sink-web`, publish `web/dist`, serve edge functions from root [`api/`](api/). Keep
`web/api/` and `web/middleware.ts` in sync with the root copies (same handlers) so a
Root Directory = `web` setup still works.

Manual prebuilt path: `cd web && vercel build --prod && vercel deploy --prebuilt --prod`.

## House standards

Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Thanks for your interest in contributing. This guide covers what you need to get
## Dev Setup

```bash
git clone https://github.com/totalaudiopromo/sink-cli.git
git clone https://github.com/chrisschouk/sink-cli.git
cd sink-cli
pnpm install
pnpm build
Expand All @@ -20,6 +20,8 @@ pnpm build
| `pnpm test` | Run all tests (Vitest) |
| `pnpm test:watch` | Tests in watch mode |
| `pnpm typecheck` | Type-check without emitting |
| `pnpm lint` | ESLint on `src/` and `test/` |
| `pnpm format:check` | Prettier check |

## Running locally

Expand Down Expand Up @@ -67,14 +69,18 @@ src/
scrub/ Email validation, parsing, typo correction
rinse/ Deduplication strategies
soak/ AI enrichment providers
steep/ Outlet channel discovery (Firecrawl + LLM)
output/ CSV/JSON/JSONL formatters
ui/ Terminal UI (format helpers, TUI, interactive)
utils/ MX cache, helpers
web/ Browser demo (Vite + React) → datasink.dev
api/ Vercel edge functions (health, Firecrawl proxy)
test/
fixtures/ Sample CSV files
scrub/ Scrub phase tests
rinse/ Rinse phase tests
soak/ Provider tests
steep/ Steep phase tests
pipeline.test.ts Integration tests
```

Expand Down
39 changes: 28 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
```

[![npm version](https://img.shields.io/npm/v/datasink.svg)](https://www.npmjs.com/package/datasink)
[![CI](https://github.com/totalaudiopromo/sink-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/totalaudiopromo/sink-cli/actions/workflows/ci.yml)
[![CI](https://github.com/chrisschouk/sink-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisschouk/sink-cli/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)

**Data hygiene for music PR.** Scrub, rinse, and soak your contact lists.
**Data hygiene for music PR.** Scrub, rinse, soak, and steep your contact lists.

> The product is **sink**; the binary is `sink`. It's published on npm as
> [`datasink`](https://www.npmjs.com/package/datasink) because the `sink` name
Expand All @@ -32,13 +32,15 @@

## Try it in the browser

**[sink-web-indol.vercel.app](https://sink-web-indol.vercel.app)** — drop a CSV
and watch the real engine run client-side. Your contacts never leave your
browser; only domain names are checked against DNS. Source in [`web/`](web/).
**[datasink.dev](https://datasink.dev)** — drop a CSV or XLSX and watch the real
engine run client-side. Scrub and rinse stay in your browser; only domain names
are checked against DNS. AI phases (soak/steep) use your own API keys. Source in
[`web/`](web/).

## Quick Start

```bash
npx datasink demo # sample data, no file needed
npx datasink scrub contacts.csv # validate emails
npx datasink rinse contacts.csv # deduplicate
npx datasink wash contacts.csv # full pipeline
Expand All @@ -56,6 +58,7 @@ sink scrub contacts.csv
| Command | Description |
| --------------------- | --------------------------------------------- |
| `sink` | Interactive menu (no args) |
| `sink demo` | Full pipeline on built-in sample data |
| `sink wash <file>` | Full pipeline: scrub + rinse + soak + steep |
| `sink scrub <file>` | Validate & clean emails |
| `sink rinse <file>` | Deduplicate contacts |
Expand All @@ -66,6 +69,10 @@ sink scrub contacts.csv
| `sink drain <file>` | Convert between formats |
| `sink tui <file>` | Full TUI dashboard |

> Soak and steep need provider keys (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`;
> steep also needs `FIRECRAWL_API_KEY`). Without keys those phases are skipped
> with a clear warning — scrub, rinse, spot, inspect, and demo still work.

## Why sink?

- **Built for music PR.** Knows BBC Radio 1 from Radio X, catches `bbc.com` → `bbc.co.uk` typos, flags role-based emails like `press@`. Not a generic email validator -- it understands your industry.
Expand Down Expand Up @@ -103,7 +110,9 @@ Enriches contacts with AI:
- Submission guidelines
- Pitch tips

Supports **Anthropic** (Claude Haiku) and **OpenAI** (GPT-4o-mini).
Supports **Anthropic** and **OpenAI** with any model ID those vendors accept.
CLI shortcuts (`haiku`, `sonnet`, `opus`, `gpt-4o-mini`, `codex`) are convenience
defaults only — use `--provider anthropic|openai --model <id>` for anything else.

### Steep

Expand All @@ -120,7 +129,8 @@ One scrape powers every contact at that outlet. The CLI caches scrapes in
memory for the duration of a run; a persistent 30-day cache is available to
programmatic consumers that supply their own `CacheAdapter` (see below).

Requires `FIRECRAWL_API_KEY` and an LLM provider key. Phase is silently skipped if creds are missing.
Requires `FIRECRAWL_API_KEY` and an LLM provider key. Skipped with a warning if
creds are missing.

## Global Flags

Expand All @@ -133,7 +143,8 @@ Requires `FIRECRAWL_API_KEY` and an LLM provider key. Phase is silently skipped
-q, --quiet Suppress all output except errors
--json JSON stdout (for piping)
--no-colour Disable colours
--provider <name> Enrichment provider (anthropic|openai)
--provider <name> LLM vendor or shortcut (anthropic|openai|haiku|sonnet|opus|codex|gpt-4o-mini)
--model <id> Any Anthropic/OpenAI model ID (overrides shortcut default)
```

## Exit Codes
Expand All @@ -148,18 +159,24 @@ Requires `FIRECRAWL_API_KEY` and an LLM provider key. Phase is silently skipped

## Provider Setup

Sink is model-agnostic across Anthropic and OpenAI. Shortcuts pick a convenient
default; `--model` accepts any current model ID from that vendor. The same
choice applies to both soak and steep.

### Anthropic

```bash
export ANTHROPIC_API_KEY=sk-ant-...
sink soak contacts.csv --provider anthropic
sink soak contacts.csv --provider haiku
sink soak contacts.csv --provider anthropic --model claude-sonnet-4-5-20250514
```

### OpenAI

```bash
export OPENAI_API_KEY=sk-...
sink soak contacts.csv --provider openai
sink soak contacts.csv --provider gpt-4o-mini
sink soak contacts.csv --provider openai --model gpt-4.1-mini
```

## Input Format
Expand Down Expand Up @@ -256,7 +273,7 @@ Tools I build for music PR, by [Chris Schofield](https://x.com/chrisschouk). Par
| [SpotCheck](https://spotcheck.cc) | Spotify playlist validation |
| [Newsjack](https://newsjack.cc) | Music industry newsjacking |
| [Podflow](https://github.com/totalaudiopromo/podflow) | Podcast intelligence for music PR |
| [Sink](https://github.com/totalaudiopromo/sink-cli) | Contact data hygiene CLI |
| [Sink](https://github.com/chrisschouk/sink-cli) | Contact data hygiene CLI |

Questions? Reach me on [X/@chrisschouk](https://x.com/chrisschouk) or [info@totalaudiopromo.com](mailto:info@totalaudiopromo.com).

Expand Down
71 changes: 71 additions & 0 deletions api/firecrawl-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Thin Firecrawl proxy — the ONLY server-side code in sink web.
*
* Firecrawl's REST API blocks browser CORS, so a single outlet scrape is
* forwarded here. The user's Firecrawl key arrives in the request body, is used
* once for the upstream call, and is never logged or stored. Everything else in
* sink web runs in the browser.
*
* Source is intentionally tiny and open — see github.com/chrisschouk/sink-cli.
*/

export const config = { runtime: 'edge' }

const FIRECRAWL_URL = 'https://api.firecrawl.dev/v1/scrape'
const TIMEOUT_MS = 15_000

function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
})
}

export default async function handler(req: Request): Promise<Response> {
if (req.method !== 'POST') return json({ error: 'Method not allowed' }, 405)

let payload: { url?: string; firecrawlKey?: string }
try {
payload = (await req.json()) as { url?: string; firecrawlKey?: string }
} catch {
return json({ error: 'Invalid JSON body' }, 400)
}

const { url, firecrawlKey } = payload
if (!url || !/^https:\/\//i.test(url)) return json({ error: 'A https url is required' }, 400)
if (!firecrawlKey || !firecrawlKey.startsWith('fc-')) {
return json({ error: 'A Firecrawl key (fc-…) is required' }, 400)
}

const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
try {
const upstream = await fetch(FIRECRAWL_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${firecrawlKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, formats: ['markdown'], onlyMainContent: true }),
signal: controller.signal,
})

if (!upstream.ok) {
// Surface auth failures so the UI can prompt for a valid key; treat other
// upstream errors as an empty (skippable) scrape.
if (upstream.status === 401 || upstream.status === 403) {
return json({ error: 'Firecrawl rejected the key' }, 401)
}
return json({ markdown: '' }, 200)
}

const data = (await upstream.json()) as {
data?: { markdown?: string; content?: string }
}
return json({ markdown: data?.data?.markdown ?? data?.data?.content ?? '' })
} catch {
return json({ markdown: '' }, 200)
} finally {
clearTimeout(timer)
}
}
23 changes: 23 additions & 0 deletions api/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Tiny health endpoint for uptime checks and the OpenAPI catalog.
* No secrets, no side effects.
*/

export const config = { runtime: 'edge' }

export default function handler(): Response {
return new Response(
JSON.stringify({
status: 'ok',
version: '0.4.0',
service: 'datasink',
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60',
},
},
)
}
49 changes: 49 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export const config = {
matcher: ['/', '/index.html'],
}

export default function middleware(request: Request): Response | void {
const accept = request.headers.get('accept') || ''
if (accept.includes('text/markdown')) {
const markdownContent = `# datasink.dev — Data Hygiene for Music PR

sink scrubs, rinses, soaks, and steeps your contact lists.

## Overview

datasink is a data hygiene CLI and browser demo for music PR contact lists.
Scrub and rinse run locally in the browser. Soak and steep use bring-your-own-key AI providers.

## Core capabilities

- **Scrub**: format validation, typo mapping, disposable domains, role accounts, MX checks
- **Rinse**: multi-field deduplication
- **Soak**: LLM contact enrichment (your API key)
- **Steep**: outlet channel discovery via Firecrawl (your API key)

## Getting started

\`\`\`bash
npx datasink demo
npx datasink scrub contacts.csv
\`\`\`

## Links

- Web demo: https://datasink.dev
- npm: https://www.npmjs.com/package/datasink
- Source: https://github.com/chrisschouk/sink-cli
- LLM docs: https://datasink.dev/llms.txt
- Auth notes: https://datasink.dev/auth.md
- Security: https://datasink.dev/.well-known/security.txt
`

return new Response(markdownContent, {
status: 200,
headers: {
'content-type': 'text/markdown; charset=utf-8',
'cache-control': 'public, max-age=3600',
},
})
}
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "datasink",
"version": "0.4.0",
"description": "sink — data hygiene for music PR. Scrub, rinse, soak, steep your contact lists. The `sink` CLI, published as datasink.",
"description": "sink \u2014 data hygiene for music PR. Scrub, rinse, soak, steep your contact lists. The `sink` CLI, published as datasink.",
"license": "MIT",
"type": "module",
"bin": {
Expand Down Expand Up @@ -76,13 +76,13 @@
"csv",
"data-hygiene"
],
"homepage": "https://github.com/totalaudiopromo/sink-cli#readme",
"homepage": "https://datasink.dev",
"bugs": {
"url": "https://github.com/totalaudiopromo/sink-cli/issues"
"url": "https://github.com/chrisschouk/sink-cli/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/totalaudiopromo/sink-cli.git"
"url": "https://github.com/chrisschouk/sink-cli.git"
},
"author": "Total Audio Promo",
"files": [
Expand Down
Loading
Loading