diff --git a/README.md b/README.md
index 98203d2..27d9b0d 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ TypeScript, installed as executables on `PATH`.
| [`free-names`](#free-names) | Name ideas nobody has registered yet, in one command |
| [`blog-post`](#blog-post) | Publish to a plain-HTML blog without breaking the feed |
| [`ask-web`](#ask-web) | Answer a question from the live web, with its sources |
+| [`terms`](#terms) | An AI lawyer for a site's terms and privacy policy: what you agree to, red flags, what to push back on |
| [`websearch`](#websearch) | One query across Perplexity, Serper (Google) and Cloudflare's Ceramic, Exa and Linkup, ranked by our own fusion; `/search` in the pit |
| [`tts`](#tts) | Read text aloud and keep the audio |
| [`affiliate`](#affiliate) | Work through a list of programs you mean to sign up for |
@@ -465,6 +466,8 @@ carries the same masked previews, not the values.
| `openai` | `OPENAI_API_KEY` | `generate-names`, `free-names` |
| `anthropic` | `ANTHROPIC_API_KEY` | `generate-names`, `free-names` |
| `perplexity` | `PERPLEXITY_API_KEY` | `ask-web` |
+| `openrouter` | `OPENROUTER_API_KEY` | `terms` (or `openai` / `anthropic`: any one) |
+| `jina` | `JINA_API_KEY` | `terms`, optional: pages only a browser can render |
| `elevenlabs` | `ELEVENLABS_API_KEY` | `tts` |
| `porkbun` | `PORKBUN_API_KEY` | `porkbun` |
| `porkbun_secret` | `PORKBUN_SECRET_API_KEY` | `porkbun` |
@@ -1134,6 +1137,39 @@ What it refuses to do:
`check` reports missing, unparseable and future dates, empty descriptions and a
missing `
`, and exits non-zero, so it works as a pre-publish gate.
+### `terms`
+
+An AI lawyer for a site's terms of service and privacy policy. Give it a
+domain or a link; it finds the terms, the privacy policy and the policies they
+pull in (acceptable use, DPA, cookies, refunds, AI terms), reads every word,
+and prints the verdict and risk score, what you agree to, the key terms
+(liability cap, indemnity, arbitration, termination, auto-renewal, content
+license, AI training), where your data goes, red flags with the clause quoted,
+what to push back on, and what to take from them for our own terms.
+
+```sh
+terms stripe.com
+terms https://example.com/legal/terms --as customer
+terms example.com --context "we will send customer PII through their API"
+terms example.com --provider openrouter --model anthropic/claude-sonnet-5.5
+terms --file agreement.txt # text you already have; - reads stdin
+terms find vercel.com # which documents it would read; no model
+terms stripe.com --json | jq .report.red_flags
+```
+
+`--as` is whose side the advice is on: `business` (default: a company putting
+the service in its product), `customer`, `developer` (building on its API) or
+`vendor` (drafting your own terms). It runs on whichever of the `anthropic`,
+`openai` or `openrouter` keys is set (`--provider` picks), defaulting to
+`claude-opus-5-5`, `gpt-5.6-sol` or `anthropic/claude-opus-5.5`; with none, it
+asks https://fineprint.agent.pit.world. A review reads 50-180k characters and
+costs about $0.50 on Opus 5.5.
+
+In moshcode it is `/terms `. The reading is
+[@profullstack/fineprint](https://github.com/profullstack/fineprint), the same
+code behind fineprint.agent, its own CLI and its MCP server. Not legal advice:
+every claim carries the clause it rests on, so check them.
+
### `ask-web`
Answers a question from a live web search and prints the pages the answer came
diff --git a/bin/terms.ts b/bin/terms.ts
new file mode 100755
index 0000000..e27d473
--- /dev/null
+++ b/bin/terms.ts
@@ -0,0 +1,111 @@
+#!/usr/bin/env node
+/**
+ * terms — an AI lawyer for a site's terms of service and privacy policy.
+ *
+ * terms stripe.com
+ * terms https://example.com/legal/terms --as customer
+ *
+ * moshcode runs this for `/terms `.
+ */
+
+import { readFileSync } from 'node:fs';
+import { findDocuments, run, runText, toMarkdown } from '@profullstack/fineprint';
+import { UsageError, parseArgs } from '../src/args.ts';
+import { resolveCredentials } from '../src/credentials.ts';
+import { isMain } from '../src/is-main.ts';
+import { PERSPECTIVES, PROVIDERS, termsOptions } from '../src/terms.ts';
+
+const USAGE = `Usage:
+ terms find the terms + privacy policy, read them, advise
+ terms --file review text you already have (a pasted agreement)
+ terms find which documents it would read (no model, free)
+
+Finds the terms of service, privacy policy and the policies they pull in
+(acceptable use, DPA, cookies, refunds, AI terms), reads every word, and says
+what you are agreeing to: the verdict and risk score, liabilities, where your
+data goes, red flags with the clause quoted, and what to push back on.
+
+Options:
+ --as SIDE ${PERSPECTIVES.join(' | ')} (default: business)
+ --context TEXT what you will use it for; sharpens the advice
+ --provider P ${PROVIDERS.join(' | ')} (default: the first key set)
+ --model ID default claude-opus-5-5 / gpt-5.6-sol / anthropic/claude-opus-5.5
+ --effort LEVEL anthropic only: low | medium | high | xhigh | max
+ --also A,B more policy links to read alongside the ones it finds
+ --brief red flags, data and asks only
+ --json the whole review as JSON
+ --remote use fineprint.agent even with a key here
+ --fresh (remote) ignore its 7-day cache
+ -h, --help show this help
+
+Runs on your own key, any one of:
+
+ cli-tools config set anthropic # or openai, or openrouter
+ cli-tools config set jina # optional: reads pages only a browser can
+
+With none, it asks https://fineprint.agent.pit.world (cached reviews free, a few
+fresh ones a day). Not legal advice: it quotes the clauses so you can check.
+
+The review goes to stdout; progress goes to stderr.
+`;
+
+if (isMain(import.meta.url)) {
+ try {
+ const parsed = parseArgs(process.argv.slice(2), {
+ boolean: ['-h', '--help', '--json', '--brief', '--remote', '--fresh'],
+ string: ['--as', '--context', '--provider', '--model', '--effort', '--also', '--file'],
+ });
+ if (parsed.flags.has('-h') || parsed.flags.has('--help') || (parsed.positional.length === 0 && !parsed.values.has('--file'))) {
+ process.stdout.write(USAGE);
+ process.exit(parsed.flags.has('-h') || parsed.flags.has('--help') ? 0 : 1);
+ }
+ const o = termsOptions(parsed);
+
+ if (o.find) {
+ const docs = await findDocuments(o.target!);
+ process.stdout.write(
+ o.json
+ ? `${JSON.stringify(docs, null, 2)}\n`
+ : docs.length
+ ? `${docs.map((d) => `${d.kind.padEnd(8)} ${d.url}`).join('\n')}\n`
+ : 'found nothing; pass the policy link itself\n',
+ );
+ process.exit(docs.length ? 0 : 1);
+ }
+
+ // Stored keys first, environment on top — see src/credentials.ts. fineprint
+ // reads JINA_API_KEY from the environment, so put a stored one there.
+ const env = resolveCredentials(process.env);
+ if (env['JINA_API_KEY'] && !process.env['JINA_API_KEY']) process.env['JINA_API_KEY'] = env['JINA_API_KEY'];
+
+ const common = {
+ env,
+ ...(o.provider ? { provider: o.provider } : {}),
+ ...(o.model ? { model: o.model } : {}),
+ ...(o.perspective ? { perspective: o.perspective } : {}),
+ ...(o.context ? { context: o.context } : {}),
+ ...(o.effort ? { effort: o.effort } : {}),
+ onStep: (message: string) => process.stderr.write(`· ${message}\n`),
+ };
+
+ const review =
+ o.file !== null
+ ? await runText(readFileSync(o.file === '-' || o.file === '' ? 0 : o.file, 'utf8'), {
+ ...common,
+ name: o.file && o.file !== '-' ? o.file : 'pasted text',
+ })
+ : await run(o.target!, { ...common, extra: o.extra, remote: o.remote, fresh: o.fresh });
+
+ process.stdout.write(o.json ? `${JSON.stringify(review, null, 2)}\n` : toMarkdown(review, { brief: o.brief }));
+ if (review.usage) {
+ process.stderr.write(`${review.provider} · ${review.model} · ${review.usage.input_tokens} in / ${review.usage.output_tokens} out\n`);
+ }
+ } catch (error) {
+ if (error instanceof UsageError) {
+ process.stderr.write(`terms: ${error.message}\n`);
+ process.exit(1);
+ }
+ process.stderr.write(`terms: ${error instanceof Error ? error.message : error}\n`);
+ process.exit(2);
+ }
+}
diff --git a/package.json b/package.json
index 4bca6c6..f54b908 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@profullstack/cli-tools",
- "version": "0.72.0",
+ "version": "0.73.0",
"private": true,
"description": "Local command-line tools, in TypeScript, exposed on PATH.",
"type": "module",
@@ -32,6 +32,7 @@
"sharp": "^0.35.3"
},
"dependencies": {
+ "@profullstack/fineprint": "^0.1.1",
"@profullstack/hqtui": "^0.6.0",
"@profullstack/proxy": "^0.2.0",
"@resvg/resvg-js": "^2.6.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 527e437..a41692d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@profullstack/fineprint':
+ specifier: ^0.1.1
+ version: 0.1.1
'@profullstack/hqtui':
specifier: ^0.6.0
version: 0.6.0
@@ -217,6 +220,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@hono/node-server@2.1.5':
+ resolution: {integrity: sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ hono: ^4 || ^5.0.0-0
+
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
@@ -382,12 +391,42 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+ '@modelcontextprotocol/sdk@1.32.2':
+ resolution: {integrity: sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@cfworker/json-schema': ^4.1.1
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ '@cfworker/json-schema':
+ optional: true
+
'@oxc-project/types@0.144.0':
resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==}
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
+ '@profullstack/fineprint@0.1.1':
+ resolution: {integrity: sha512-yoDURaDZCKoe1Z4rRFPil+fVjG/SAKf27ETQmKbSJPD2Vchz1aPxKP3OiKtsYEjOF9jOoGrrSIAtA73bs8l6VQ==}
+ engines: {bun: '>=1.1', node: '>=22'}
+ hasBin: true
+
+ '@profullstack/footer@0.4.0':
+ resolution: {integrity: sha512-EJZnMUQmWDi9Gzx6BAjogloeaPFFSSCHtHJuC9dsXiOz8M87hfn6Z+Zx9AMYWk8s6xemPrPlkXKGB3PZugkcEA==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ react: '>=18'
+ peerDependenciesMeta:
+ react:
+ optional: true
+
+ '@profullstack/hqtui@0.11.1':
+ resolution: {integrity: sha512-c/UzTmiRDjdLZQEO/gdVinUKtx6Q0WBPdixIm90MBTvC3DqFPqpersRs0BIDPS7XVokCSr1DtELI91kj5J1kCg==}
+ engines: {bun: '>=1.1', node: '>=22.6'}
+ hasBin: true
+
'@profullstack/hqtui@0.6.0':
resolution: {integrity: sha512-jUFockH6yWeE48xD4gOKINPGzUmShn5k8c6Wqv/apkxI8CIJW4ieGZx5Co+ltD/CGqnGEVnXUbUu5SlNpfBdfQ==}
engines: {bun: '>=1.1', node: '>=22.6'}
@@ -629,6 +668,21 @@ packages:
'@zone-eu/mailsplit@5.4.16':
resolution: {integrity: sha512-zQ9iXvlT3Wi/hazeC1MdI4rQc1UJwJ6IQ6QzSZ5KDxLZZWQSazWLOzImLFluXadKShJ9WJvI1xH+AyVS8b9azg==}
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv@8.20.0:
+ resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -641,17 +695,74 @@ packages:
resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==}
engines: {node: '>=4'}
+ body-parser@2.4.0:
+ resolution: {integrity: sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==}
+ engines: {node: '>=18'}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
+ content-disposition@2.0.1:
+ resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==}
+ engines: {node: '>=18'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ content-type@2.1.0:
+ resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==}
+ engines: {node: '>=18'}
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
+ engines: {node: '>= 0.10'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
deepmerge-ts@8.0.2:
resolution: {integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==}
engines: {node: '>=16.9.0'}
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -669,6 +780,17 @@ packages:
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
encoding-japanese@2.3.0:
resolution: {integrity: sha512-eQyh1vzHz13DUkZcJO+0IOAoKXRQwKV5IBffeuYsWZyRLGiSzfzXObCqWvqFXdX0UU8qOk+lBXbkUhMCpdJe4Q==}
engines: {node: '>=18.0.0'}
@@ -681,21 +803,64 @@ packages:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
es-module-lexer@2.3.1:
resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
+ es-object-atoms@1.1.2:
+ resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
+ engines: {node: '>= 0.4'}
+
esbuild@0.28.2:
resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
engines: {node: '>=18'}
hasBin: true
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ eventsource-parser@3.1.1:
+ resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==}
+ engines: {node: '>=18.0.0'}
+
+ eventsource@3.0.7:
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
+ engines: {node: '>=18.0.0'}
+
expect-type@1.4.0:
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
+ express-rate-limit@8.7.1:
+ resolution: {integrity: sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
+ express@5.3.0:
+ resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==}
+ engines: {node: '>= 18'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-uri@3.1.9:
+ resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==}
+
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -705,15 +870,54 @@ packages:
picomatch:
optional: true
+ finalhandler@2.1.1:
+ resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
+ engines: {node: '>= 18.0.0'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@2.0.0:
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
+ engines: {node: '>= 0.8'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
+ hono@4.13.14:
+ resolution: {integrity: sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==}
+ engines: {node: '>=16.9.0'}
+
html-to-text@10.0.1:
resolution: {integrity: sha512-GiVhRI1BatGARSCmlXWNCjDT0cWrwBWoeduLoV0WSKAgaV/wa+hUWy5LiQLUs4UwiUrE52ZCMfBGiKD87TDPrg==}
engines: {node: '>=20.19.0'}
@@ -721,6 +925,10 @@ packages:
htmlparser2@10.1.0:
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
+ http-errors@2.0.1:
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
+ engines: {node: '>= 0.8'}
+
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -732,10 +940,32 @@ packages:
imapflow@1.7.8:
resolution: {integrity: sha512-dJoCIdZOJh26Rn2PdwEzwj0bRDgGBxxX38pio534FagIHVuR2l0SAfLr6sJo32YfuJHiSs/U2ntNDHnMg3/Hlg==}
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
ip-address@10.7.0:
resolution: {integrity: sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==}
engines: {node: '>= 12'}
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jose@6.2.13:
+ resolution: {integrity: sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-schema-typed@8.0.2:
+ resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
+
leac@0.7.0:
resolution: {integrity: sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==}
@@ -831,11 +1061,38 @@ packages:
mailparser@3.9.20:
resolution: {integrity: sha512-PZ9RD6B7SkmyQ9rj8JvYNS18rL6pWoNkSw9KGVuJQrrdouFxFIB05ugnR3ubInlnjLXV3KHXhGyz8MlTz1VGzw==}
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ media-typer@1.1.1:
+ resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==}
+ engines: {node: '>= 0.8'}
+
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
+ mime-db@1.54.0:
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@3.0.2:
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
+ engines: {node: '>=18'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
nanoid@3.3.18:
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ negotiator@1.1.0:
+ resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==}
+ engines: {node: '>=18'}
+
nodemailer@10.0.0:
resolution: {integrity: sha512-wdv+hXBg0iIPIOf108FaZyTxqjQpCvQeAJt9pHHTooPy/y3vQtI9eiImM9AIVKR3e/0FeMA1oueNfBjFJcaK+Q==}
engines: {node: '>=20.0.0'}
@@ -844,6 +1101,14 @@ packages:
resolution: {integrity: sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==}
engines: {node: '>=6.0.0'}
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
obug@2.1.4:
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'}
@@ -852,9 +1117,27 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
+ on-finished@2.4.2:
+ resolution: {integrity: sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
parseley@0.13.1:
resolution: {integrity: sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==}
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-to-regexp@8.4.2:
+ resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -878,6 +1161,10 @@ packages:
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
hasBin: true
+ pkce-challenge@5.0.1:
+ resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
+ engines: {node: '>=16.20.0'}
+
postcss@8.5.26:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -889,13 +1176,29 @@ packages:
process-warning@5.1.0:
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
+ engines: {node: '>= 0.10'}
+
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
+ qs@6.17.0:
+ resolution: {integrity: sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==}
+ engines: {node: '>=0.6'}
+
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+ range-parser@1.3.0:
+ resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@3.0.2:
+ resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
+ engines: {node: '>= 0.10'}
+
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@@ -903,11 +1206,19 @@ packages:
real-require@1.0.0:
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
rolldown@1.2.4:
resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
@@ -923,6 +1234,17 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ send@1.2.1:
+ resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
+ engines: {node: '>= 18'}
+
+ serve-static@2.2.1:
+ resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
+ engines: {node: '>= 18'}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
sharp@0.35.3:
resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
engines: {node: '>=20.9.0'}
@@ -932,6 +1254,30 @@ packages:
'@types/node':
optional: true
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.1:
+ resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.1:
+ resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -957,6 +1303,10 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
std-env@4.2.0:
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
@@ -983,6 +1333,10 @@ packages:
resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
hasBin: true
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -991,6 +1345,10 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
+ type-is@2.1.0:
+ resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
+ engines: {node: '>= 18'}
+
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -1006,6 +1364,14 @@ packages:
resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==}
engines: {node: '>=22.19.0'}
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
vite@8.2.1:
resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1090,11 +1456,27 @@ packages:
jsdom:
optional: true
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ zod-to-json-schema@3.25.2:
+ resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
+ peerDependencies:
+ zod: ^3.25.28 || ^4
+
+ zod@4.6.7:
+ resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
+
snapshots:
'@emnapi/runtime@1.11.3':
@@ -1180,6 +1562,10 @@ snapshots:
'@esbuild/win32-x64@0.28.2':
optional: true
+ '@hono/node-server@2.1.5(hono@4.13.14)':
+ dependencies:
+ hono: 4.13.14
+
'@img/colour@1.1.0':
optional: true
@@ -1289,10 +1675,48 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.5': {}
+ '@modelcontextprotocol/sdk@1.32.2(zod@4.6.7)':
+ dependencies:
+ '@hono/node-server': 2.1.5(hono@4.13.14)
+ ajv: 8.20.0
+ ajv-formats: 3.0.1
+ content-type: 1.0.5
+ cors: 2.8.6
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.1.1
+ express: 5.3.0
+ express-rate-limit: 8.7.1(express@5.3.0)
+ hono: 4.13.14
+ jose: 6.2.13
+ json-schema-typed: 8.0.2
+ pkce-challenge: 5.0.1
+ raw-body: 3.0.2
+ zod: 4.6.7
+ zod-to-json-schema: 3.25.2(zod@4.6.7)
+ transitivePeerDependencies:
+ - supports-color
+
'@oxc-project/types@0.144.0': {}
'@pinojs/redact@0.4.0': {}
+ '@profullstack/fineprint@0.1.1':
+ dependencies:
+ '@modelcontextprotocol/sdk': 1.32.2(zod@4.6.7)
+ '@profullstack/footer': 0.4.0
+ '@profullstack/hqtui': 0.11.1
+ hono: 4.13.14
+ zod: 4.6.7
+ transitivePeerDependencies:
+ - '@cfworker/json-schema'
+ - react
+ - supports-color
+
+ '@profullstack/footer@0.4.0': {}
+
+ '@profullstack/hqtui@0.11.1': {}
+
'@profullstack/hqtui@0.6.0': {}
'@profullstack/proxy@0.2.0':
@@ -1471,18 +1895,87 @@ snapshots:
libmime: 5.4.3
libqp: 2.1.1
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.2
+ negotiator: 1.1.0
+
+ ajv-formats@3.0.1:
+ dependencies:
+ ajv: 8.20.0
+
+ ajv@8.20.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.9
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
assertion-error@2.0.1: {}
atomic-sleep@1.0.0: {}
axe-core@4.13.0: {}
+ body-parser@2.4.0:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 2.1.0
+ debug: 4.4.3
+ http-errors: 2.0.1
+ iconv-lite: 0.7.3
+ on-finished: 2.4.2
+ qs: 6.17.0
+ raw-body: 3.0.2
+ type-is: 2.1.0
+ transitivePeerDependencies:
+ - supports-color
+
+ bytes@3.1.2: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
chai@6.2.2: {}
+ content-disposition@2.0.1: {}
+
+ content-type@1.0.5: {}
+
+ content-type@2.1.0: {}
+
convert-source-map@2.0.0: {}
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
+ cors@2.8.6:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
deepmerge-ts@8.0.2: {}
+ depd@2.0.0: {}
+
detect-libc@2.1.2: {}
dom-serializer@2.0.0:
@@ -1503,14 +1996,32 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ ee-first@1.1.1: {}
+
+ encodeurl@2.0.0: {}
+
encoding-japanese@2.3.0: {}
entities@4.5.0: {}
entities@7.0.1: {}
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
es-module-lexer@2.3.1: {}
+ es-object-atoms@1.1.2:
+ dependencies:
+ es-errors: 1.3.0
+
esbuild@0.28.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.2
@@ -1540,21 +2051,121 @@ snapshots:
'@esbuild/win32-ia32': 0.28.2
'@esbuild/win32-x64': 0.28.2
+ escape-html@1.0.3: {}
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
+ etag@1.8.1: {}
+
+ eventsource-parser@3.1.1: {}
+
+ eventsource@3.0.7:
+ dependencies:
+ eventsource-parser: 3.1.1
+
expect-type@1.4.0: {}
+ express-rate-limit@8.7.1(express@5.3.0):
+ dependencies:
+ debug: 4.4.3
+ express: 5.3.0
+ ip-address: 10.7.0
+ transitivePeerDependencies:
+ - supports-color
+
+ express@5.3.0:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.4.0
+ content-disposition: 2.0.1
+ content-type: 2.1.0
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.3
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.2
+ on-finished: 2.4.2
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.8
+ qs: 6.17.0
+ range-parser: 1.3.0
+ router: 2.2.0
+ send: 1.2.1
+ serve-static: 2.2.1
+ statuses: 2.0.2
+ type-is: 2.1.0
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-uri@3.1.9: {}
+
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
+ finalhandler@2.1.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.2
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ forwarded@0.2.0: {}
+
+ fresh@2.0.0: {}
+
fsevents@2.3.3:
optional: true
+ function-bind@1.1.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.4
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.2
+
+ gopd@1.2.0: {}
+
+ has-symbols@1.1.0: {}
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
he@1.2.0: {}
+ hono@4.13.14: {}
+
html-to-text@10.0.1:
dependencies:
'@selderee/plugin-htmlparser2': 0.12.0(selderee@0.12.0)
@@ -1570,6 +2181,14 @@ snapshots:
domutils: 3.2.2
entities: 7.0.1
+ http-errors@2.0.1:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.2
+ toidentifier: 1.0.1
+
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -1589,8 +2208,22 @@ snapshots:
pino: 10.3.1
socks: 2.8.9
+ inherits@2.0.4: {}
+
ip-address@10.7.0: {}
+ ipaddr.js@1.9.1: {}
+
+ is-promise@4.0.0: {}
+
+ isexe@2.0.0: {}
+
+ jose@6.2.13: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-schema-typed@8.0.2: {}
+
leac@0.7.0: {}
libbase64@1.3.0: {}
@@ -1674,21 +2307,57 @@ snapshots:
punycode.js: 2.3.1
tlds: 1.261.0
+ math-intrinsics@1.1.0: {}
+
+ media-typer@1.1.1: {}
+
+ merge-descriptors@2.0.0: {}
+
+ mime-db@1.54.0: {}
+
+ mime-types@3.0.2:
+ dependencies:
+ mime-db: 1.54.0
+
+ ms@2.1.3: {}
+
nanoid@3.3.18: {}
+ negotiator@1.1.0:
+ dependencies:
+ content-type: 2.1.0
+
nodemailer@10.0.0: {}
nodemailer@9.1.1: {}
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
obug@2.1.4: {}
on-exit-leak-free@2.1.2: {}
+ on-finished@2.4.2:
+ dependencies:
+ ee-first: 1.1.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
parseley@0.13.1:
dependencies:
leac: 0.7.0
peberminta: 0.10.0
+ parseurl@1.3.3: {}
+
+ path-key@3.1.1: {}
+
+ path-to-regexp@8.4.2: {}
+
pathe@2.0.3: {}
peberminta@0.10.0: {}
@@ -1717,6 +2386,8 @@ snapshots:
sonic-boom: 4.2.1
thread-stream: 4.2.0
+ pkce-challenge@5.0.1: {}
+
postcss@8.5.26:
dependencies:
nanoid: 3.3.18
@@ -1727,14 +2398,35 @@ snapshots:
process-warning@5.1.0: {}
+ proxy-addr@2.0.8:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
punycode.js@2.3.1: {}
+ qs@6.17.0:
+ dependencies:
+ es-define-property: 1.0.1
+ side-channel: 1.1.1
+
quick-format-unescaped@4.0.4: {}
+ range-parser@1.3.0: {}
+
+ raw-body@3.0.2:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.1
+ iconv-lite: 0.7.3
+ unpipe: 1.0.0
+
real-require@0.2.0: {}
real-require@1.0.0: {}
+ require-from-string@2.0.2: {}
+
rolldown@1.2.4:
dependencies:
'@oxc-project/types': 0.144.0
@@ -1755,6 +2447,16 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.2.4
'@rolldown/binding-win32-x64-msvc': 1.2.4
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.3
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.4.2
+ transitivePeerDependencies:
+ - supports-color
+
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {}
@@ -1766,6 +2468,33 @@ snapshots:
semver@7.8.5:
optional: true
+ send@1.2.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ mime-types: 3.0.2
+ ms: 2.1.3
+ on-finished: 2.4.2
+ range-parser: 1.3.0
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ serve-static@2.2.1:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 1.2.1
+ transitivePeerDependencies:
+ - supports-color
+
+ setprototypeof@1.2.0: {}
+
sharp@0.35.3(@types/node@22.20.1):
dependencies:
'@img/colour': 1.1.0
@@ -1800,6 +2529,40 @@ snapshots:
'@types/node': 22.20.1
optional: true
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.1
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
siginfo@2.0.0: {}
smart-buffer@4.2.0: {}
@@ -1819,6 +2582,8 @@ snapshots:
stackback@0.0.2: {}
+ statuses@2.0.2: {}
+
std-env@4.2.0: {}
thread-stream@4.2.0:
@@ -1838,6 +2603,8 @@ snapshots:
tlds@1.261.0: {}
+ toidentifier@1.0.1: {}
+
tslib@2.8.1:
optional: true
@@ -1847,6 +2614,12 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ type-is@2.1.0:
+ dependencies:
+ content-type: 2.1.0
+ media-typer: 1.1.1
+ mime-types: 3.0.2
+
typescript@5.9.3: {}
uc.micro@2.1.0: {}
@@ -1855,6 +2628,10 @@ snapshots:
undici@8.11.2: {}
+ unpipe@1.0.0: {}
+
+ vary@1.1.2: {}
+
vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12):
dependencies:
lightningcss: 1.33.0
@@ -1895,7 +2672,19 @@ snapshots:
transitivePeerDependencies:
- msw
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
+
+ wrappy@1.0.2: {}
+
+ zod-to-json-schema@3.25.2(zod@4.6.7):
+ dependencies:
+ zod: 4.6.7
+
+ zod@4.6.7: {}
diff --git a/src/credentials.ts b/src/credentials.ts
index e759b4b..f7919d4 100644
--- a/src/credentials.ts
+++ b/src/credentials.ts
@@ -31,6 +31,11 @@ export const KNOWN_KEYS: Record = {
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
perplexity: 'PERPLEXITY_API_KEY',
+ // Read by `terms` (with openai/anthropic): any one of the three runs the review.
+ openrouter: 'OPENROUTER_API_KEY',
+ // Read by `terms`: the rendered-page fallback, for policy pages a plain fetch
+ // cannot read. Optional; anonymous use is refused from some networks.
+ jina: 'JINA_API_KEY',
elevenlabs: 'ELEVENLABS_API_KEY',
porkbun: 'PORKBUN_API_KEY',
porkbun_secret: 'PORKBUN_SECRET_API_KEY',
diff --git a/src/registry.ts b/src/registry.ts
index c6781e8..d4e959f 100644
--- a/src/registry.ts
+++ b/src/registry.ts
@@ -47,6 +47,7 @@ const SUMMARIES: Record = {
'email-cleaner': 'Clean a mailing list: drop bad, role, disposable, duplicate and unlikely addresses',
emoji: 'Every standard emoji, drawn by an image model as one set: PNG, SVG, fonts, OpenEmoji manifest',
crawlproof: 'What the fleet costs and what it returns: traffic, ads and the bank behind them',
+ terms: 'An AI lawyer for a site\'s terms and privacy policy: what you agree to, red flags, what to push back on',
submitfleet: 'Launch products on 339 launch sites and directories, tracked until live (submitfleet.com)',
'free-names': 'Name ideas nobody has registered yet, in one command',
'generate-names': 'Turn a sentence about a product into a thousand candidate names',
diff --git a/src/terms.ts b/src/terms.ts
new file mode 100644
index 0000000..4204fc7
--- /dev/null
+++ b/src/terms.ts
@@ -0,0 +1,62 @@
+/**
+ * `terms`: an AI lawyer for a site's terms of service and privacy policy.
+ *
+ * The reading happens in @profullstack/fineprint (the same code behind
+ * fineprint.agent, its CLI and its MCP server). This file only turns the
+ * command line and the keys stored here into its options, so moshcode's
+ * `/terms ` and a shell get exactly the same review.
+ */
+import { UsageError, csv, type ParsedArgs } from './args.ts';
+
+export const PERSPECTIVES = ['business', 'customer', 'developer', 'vendor'] as const;
+export const PROVIDERS = ['anthropic', 'openai', 'openrouter'] as const;
+export type Perspective = (typeof PERSPECTIVES)[number];
+
+export interface TermsOptions {
+ target: string | null;
+ file: string | null;
+ provider?: string;
+ model?: string;
+ perspective?: Perspective;
+ context?: string;
+ effort?: string;
+ extra: string[];
+ remote: boolean;
+ fresh: boolean;
+ find: boolean;
+ json: boolean;
+ brief: boolean;
+}
+
+export function termsOptions({ flags, values, positional }: ParsedArgs): TermsOptions {
+ const [first, ...rest] = positional;
+ const find = first === 'find';
+ const target = (find ? rest[0] : first) ?? null;
+ const file = values.get('--file') ?? null;
+ if (!target && file === null) throw new UsageError('give a domain or a link to the terms, or --file');
+
+ const perspective = values.get('--as');
+ if (perspective !== undefined && !PERSPECTIVES.includes(perspective as never)) {
+ throw new UsageError(`--as must be ${PERSPECTIVES.join(', ')}, got "${perspective}"`);
+ }
+ const provider = values.get('--provider');
+ if (provider !== undefined && !PROVIDERS.includes(provider as never)) {
+ throw new UsageError(`--provider must be ${PROVIDERS.join(', ')}, got "${provider}"`);
+ }
+
+ return {
+ target,
+ file,
+ ...(provider ? { provider } : {}),
+ ...(values.get('--model') ? { model: values.get('--model')! } : {}),
+ ...(perspective ? { perspective: perspective as Perspective } : {}),
+ ...(values.get('--context') ? { context: values.get('--context')! } : {}),
+ ...(values.get('--effort') ? { effort: values.get('--effort')! } : {}),
+ extra: csv(values, '--also'),
+ remote: flags.has('--remote'),
+ fresh: flags.has('--fresh'),
+ find,
+ json: flags.has('--json'),
+ brief: flags.has('--brief'),
+ };
+}
diff --git a/test/terms.test.ts b/test/terms.test.ts
new file mode 100644
index 0000000..269dcd0
--- /dev/null
+++ b/test/terms.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest';
+import { UsageError, parseArgs } from '../src/args.ts';
+import { termsOptions } from '../src/terms.ts';
+
+const spec = {
+ boolean: ['--json', '--brief', '--remote', '--fresh'],
+ string: ['--as', '--context', '--provider', '--model', '--effort', '--also', '--file'],
+} as const;
+const opts = (...argv: string[]) => termsOptions(parseArgs(argv, spec));
+
+describe('terms options', () => {
+ it('takes a domain and the flags fineprint needs', () => {
+ expect(opts('stripe.com', '--as', 'customer', '--provider', 'openrouter', '--also', 'https://a.test/dpa, https://a.test/aup', '--brief')).toEqual({
+ target: 'stripe.com',
+ file: null,
+ provider: 'openrouter',
+ perspective: 'customer',
+ extra: ['https://a.test/dpa', 'https://a.test/aup'],
+ remote: false,
+ fresh: false,
+ find: false,
+ json: false,
+ brief: true,
+ });
+ });
+
+ it('find is discovery only', () => {
+ expect(opts('find', 'vercel.com')).toMatchObject({ target: 'vercel.com', find: true });
+ });
+
+ it('a file stands in for a target', () => {
+ expect(opts('--file', 'agreement.txt')).toMatchObject({ target: null, file: 'agreement.txt' });
+ });
+
+ it('rejects an unknown side or provider, and no input at all', () => {
+ expect(() => opts('x.com', '--as', 'lawyer')).toThrow(UsageError);
+ expect(() => opts('x.com', '--provider', 'gemini')).toThrow(/anthropic, openai, openrouter/);
+ expect(() => opts('--json')).toThrow(UsageError);
+ });
+});