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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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` |
Expand Down Expand Up @@ -1134,6 +1137,39 @@ What it refuses to do:
`check` reports missing, unparseable and future dates, empty descriptions and a
missing `<h1>`, 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 <domain>`. 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
Expand Down
111 changes: 111 additions & 0 deletions bin/terms.ts
Original file line number Diff line number Diff line change
@@ -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 <domain>`.
*/

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 <domain|link> find the terms + privacy policy, read them, advise
terms --file <path|-> review text you already have (a pasted agreement)
terms find <domain> 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);
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading