Skip to content

Commit f7f9187

Browse files
sh1pt-actions-fleet[bot]ralyodioclaude
authored
terms: an AI lawyer for a site's terms and privacy policy (0.73.0) (#215)
`terms <domain|link>` finds the terms, privacy policy and the policies they pull in, reads them and prints the verdict, what you agree to, key terms, where your data goes, red flags with the clause quoted and what to push back on. Runs @profullstack/fineprint on the stored anthropic, openai or openrouter key (openrouter + jina join the credential store); with none it asks fineprint.agent. moshcode runs it for /terms. Co-authored-by: Anthony Ettinger <anthony@profullstack.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5634248 commit f7f9187

8 files changed

Lines changed: 1046 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ TypeScript, installed as executables on `PATH`.
1515
| [`free-names`](#free-names) | Name ideas nobody has registered yet, in one command |
1616
| [`blog-post`](#blog-post) | Publish to a plain-HTML blog without breaking the feed |
1717
| [`ask-web`](#ask-web) | Answer a question from the live web, with its sources |
18+
| [`terms`](#terms) | An AI lawyer for a site's terms and privacy policy: what you agree to, red flags, what to push back on |
1819
| [`websearch`](#websearch) | One query across Perplexity, Serper (Google) and Cloudflare's Ceramic, Exa and Linkup, ranked by our own fusion; `/search` in the pit |
1920
| [`tts`](#tts) | Read text aloud and keep the audio |
2021
| [`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.
465466
| `openai` | `OPENAI_API_KEY` | `generate-names`, `free-names` |
466467
| `anthropic` | `ANTHROPIC_API_KEY` | `generate-names`, `free-names` |
467468
| `perplexity` | `PERPLEXITY_API_KEY` | `ask-web` |
469+
| `openrouter` | `OPENROUTER_API_KEY` | `terms` (or `openai` / `anthropic`: any one) |
470+
| `jina` | `JINA_API_KEY` | `terms`, optional: pages only a browser can render |
468471
| `elevenlabs` | `ELEVENLABS_API_KEY` | `tts` |
469472
| `porkbun` | `PORKBUN_API_KEY` | `porkbun` |
470473
| `porkbun_secret` | `PORKBUN_SECRET_API_KEY` | `porkbun` |
@@ -1134,6 +1137,39 @@ What it refuses to do:
11341137
`check` reports missing, unparseable and future dates, empty descriptions and a
11351138
missing `<h1>`, and exits non-zero, so it works as a pre-publish gate.
11361139

1140+
### `terms`
1141+
1142+
An AI lawyer for a site's terms of service and privacy policy. Give it a
1143+
domain or a link; it finds the terms, the privacy policy and the policies they
1144+
pull in (acceptable use, DPA, cookies, refunds, AI terms), reads every word,
1145+
and prints the verdict and risk score, what you agree to, the key terms
1146+
(liability cap, indemnity, arbitration, termination, auto-renewal, content
1147+
license, AI training), where your data goes, red flags with the clause quoted,
1148+
what to push back on, and what to take from them for our own terms.
1149+
1150+
```sh
1151+
terms stripe.com
1152+
terms https://example.com/legal/terms --as customer
1153+
terms example.com --context "we will send customer PII through their API"
1154+
terms example.com --provider openrouter --model anthropic/claude-sonnet-5.5
1155+
terms --file agreement.txt # text you already have; - reads stdin
1156+
terms find vercel.com # which documents it would read; no model
1157+
terms stripe.com --json | jq .report.red_flags
1158+
```
1159+
1160+
`--as` is whose side the advice is on: `business` (default: a company putting
1161+
the service in its product), `customer`, `developer` (building on its API) or
1162+
`vendor` (drafting your own terms). It runs on whichever of the `anthropic`,
1163+
`openai` or `openrouter` keys is set (`--provider` picks), defaulting to
1164+
`claude-opus-5-5`, `gpt-5.6-sol` or `anthropic/claude-opus-5.5`; with none, it
1165+
asks https://fineprint.agent.pit.world. A review reads 50-180k characters and
1166+
costs about $0.50 on Opus 5.5.
1167+
1168+
In moshcode it is `/terms <domain>`. The reading is
1169+
[@profullstack/fineprint](https://github.com/profullstack/fineprint), the same
1170+
code behind fineprint.agent, its own CLI and its MCP server. Not legal advice:
1171+
every claim carries the clause it rests on, so check them.
1172+
11371173
### `ask-web`
11381174

11391175
Answers a question from a live web search and prints the pages the answer came

‎bin/terms.ts‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env node
2+
/**
3+
* terms — an AI lawyer for a site's terms of service and privacy policy.
4+
*
5+
* terms stripe.com
6+
* terms https://example.com/legal/terms --as customer
7+
*
8+
* moshcode runs this for `/terms <domain>`.
9+
*/
10+
11+
import { readFileSync } from 'node:fs';
12+
import { findDocuments, run, runText, toMarkdown } from '@profullstack/fineprint';
13+
import { UsageError, parseArgs } from '../src/args.ts';
14+
import { resolveCredentials } from '../src/credentials.ts';
15+
import { isMain } from '../src/is-main.ts';
16+
import { PERSPECTIVES, PROVIDERS, termsOptions } from '../src/terms.ts';
17+
18+
const USAGE = `Usage:
19+
terms <domain|link> find the terms + privacy policy, read them, advise
20+
terms --file <path|-> review text you already have (a pasted agreement)
21+
terms find <domain> which documents it would read (no model, free)
22+
23+
Finds the terms of service, privacy policy and the policies they pull in
24+
(acceptable use, DPA, cookies, refunds, AI terms), reads every word, and says
25+
what you are agreeing to: the verdict and risk score, liabilities, where your
26+
data goes, red flags with the clause quoted, and what to push back on.
27+
28+
Options:
29+
--as SIDE ${PERSPECTIVES.join(' | ')} (default: business)
30+
--context TEXT what you will use it for; sharpens the advice
31+
--provider P ${PROVIDERS.join(' | ')} (default: the first key set)
32+
--model ID default claude-opus-5-5 / gpt-5.6-sol / anthropic/claude-opus-5.5
33+
--effort LEVEL anthropic only: low | medium | high | xhigh | max
34+
--also A,B more policy links to read alongside the ones it finds
35+
--brief red flags, data and asks only
36+
--json the whole review as JSON
37+
--remote use fineprint.agent even with a key here
38+
--fresh (remote) ignore its 7-day cache
39+
-h, --help show this help
40+
41+
Runs on your own key, any one of:
42+
43+
cli-tools config set anthropic # or openai, or openrouter
44+
cli-tools config set jina # optional: reads pages only a browser can
45+
46+
With none, it asks https://fineprint.agent.pit.world (cached reviews free, a few
47+
fresh ones a day). Not legal advice: it quotes the clauses so you can check.
48+
49+
The review goes to stdout; progress goes to stderr.
50+
`;
51+
52+
if (isMain(import.meta.url)) {
53+
try {
54+
const parsed = parseArgs(process.argv.slice(2), {
55+
boolean: ['-h', '--help', '--json', '--brief', '--remote', '--fresh'],
56+
string: ['--as', '--context', '--provider', '--model', '--effort', '--also', '--file'],
57+
});
58+
if (parsed.flags.has('-h') || parsed.flags.has('--help') || (parsed.positional.length === 0 && !parsed.values.has('--file'))) {
59+
process.stdout.write(USAGE);
60+
process.exit(parsed.flags.has('-h') || parsed.flags.has('--help') ? 0 : 1);
61+
}
62+
const o = termsOptions(parsed);
63+
64+
if (o.find) {
65+
const docs = await findDocuments(o.target!);
66+
process.stdout.write(
67+
o.json
68+
? `${JSON.stringify(docs, null, 2)}\n`
69+
: docs.length
70+
? `${docs.map((d) => `${d.kind.padEnd(8)} ${d.url}`).join('\n')}\n`
71+
: 'found nothing; pass the policy link itself\n',
72+
);
73+
process.exit(docs.length ? 0 : 1);
74+
}
75+
76+
// Stored keys first, environment on top — see src/credentials.ts. fineprint
77+
// reads JINA_API_KEY from the environment, so put a stored one there.
78+
const env = resolveCredentials(process.env);
79+
if (env['JINA_API_KEY'] && !process.env['JINA_API_KEY']) process.env['JINA_API_KEY'] = env['JINA_API_KEY'];
80+
81+
const common = {
82+
env,
83+
...(o.provider ? { provider: o.provider } : {}),
84+
...(o.model ? { model: o.model } : {}),
85+
...(o.perspective ? { perspective: o.perspective } : {}),
86+
...(o.context ? { context: o.context } : {}),
87+
...(o.effort ? { effort: o.effort } : {}),
88+
onStep: (message: string) => process.stderr.write(`· ${message}\n`),
89+
};
90+
91+
const review =
92+
o.file !== null
93+
? await runText(readFileSync(o.file === '-' || o.file === '' ? 0 : o.file, 'utf8'), {
94+
...common,
95+
name: o.file && o.file !== '-' ? o.file : 'pasted text',
96+
})
97+
: await run(o.target!, { ...common, extra: o.extra, remote: o.remote, fresh: o.fresh });
98+
99+
process.stdout.write(o.json ? `${JSON.stringify(review, null, 2)}\n` : toMarkdown(review, { brief: o.brief }));
100+
if (review.usage) {
101+
process.stderr.write(`${review.provider} · ${review.model} · ${review.usage.input_tokens} in / ${review.usage.output_tokens} out\n`);
102+
}
103+
} catch (error) {
104+
if (error instanceof UsageError) {
105+
process.stderr.write(`terms: ${error.message}\n`);
106+
process.exit(1);
107+
}
108+
process.stderr.write(`terms: ${error instanceof Error ? error.message : error}\n`);
109+
process.exit(2);
110+
}
111+
}

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@profullstack/cli-tools",
3-
"version": "0.72.0",
3+
"version": "0.73.0",
44
"private": true,
55
"description": "Local command-line tools, in TypeScript, exposed on PATH.",
66
"type": "module",
@@ -32,6 +32,7 @@
3232
"sharp": "^0.35.3"
3333
},
3434
"dependencies": {
35+
"@profullstack/fineprint": "^0.1.1",
3536
"@profullstack/hqtui": "^0.6.0",
3637
"@profullstack/proxy": "^0.2.0",
3738
"@resvg/resvg-js": "^2.6.2",

0 commit comments

Comments
 (0)