@readystack/email-footer-law-lint 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -3
  2. package/cli.js +51 -2
  3. package/package.json +7 -1
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # Email Footer Law Lint
2
2
 
3
- ![Email Footer Law Lint](https://getreadystack.com/img/promo/sku40591_result_card.jpg)
4
-
5
3
  Fourteen checks on an HTML email footer: CAN-SPAM postal address and 10-business-day opt-out, CASL's 60-day window, and German 5 DDG, the statute that replaced 5 TMG on 2024-05-14.
6
4
 
7
5
  ## Install
@@ -27,7 +25,26 @@ Node 18+. The same 14 rules as the VS Code extension, from a terminal or CI.
27
25
 
28
26
  Outside counsel reads one email footer against CAN-SPAM, CASL and 5 DDG at about $300 an hour, and reads it once.
29
27
 
30
- 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
28
+ ## Use from an AI agent (MCP)
29
+
30
+ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
31
+
32
+ ```json
33
+ { "mcpServers": { "email-footer-law-lint": { "command": "npx", "args": ["-y", "@readystack/email-footer-law-lint", "--mcp"] } } }
34
+ ```
35
+
36
+ Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
37
+
38
+ ## Use in CI
39
+
40
+ ```yaml
41
+ - name: Email Footer Law Lint
42
+ run: npx -y @readystack/email-footer-law-lint --dir . --ci
43
+ ```
44
+
45
+ (container: `docker run --rm -v "$PWD:/work" getreadystack/email-footer-law-lint --dir /work --ci`)
46
+
47
+ Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
31
48
 
32
49
  [Get a licence](https://buy.polar.sh/polar_cl_UpcPQkomm2d1N52PaYHHfPjdB89okIRPLaTa70CzAj0)
33
50
 
package/cli.js CHANGED
@@ -38,19 +38,64 @@ function render(rows, fmt) {
38
38
  let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
39
39
  return o;
40
40
  }
41
+ function trialState() {
42
+ if (process.env.READYSTACK_NO_TRIAL) return { active: false };
43
+ try {
44
+ const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
45
+ let t = null; try { t = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) {}
46
+ if (!t || !t.until) { t = { until: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 10) }; fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(t)); }
47
+ return { active: new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t.until };
48
+ } catch (e) { return { active: false }; }
49
+ }
50
+ function mcpServe() {
51
+ // s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (7-day trial): check_dir.
52
+ const rl = require('readline').createInterface({ input: process.stdin });
53
+ const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
54
+ const tools = [
55
+ { name: 'check_text', description: S.name + ' - run all ' + RULE_N + ' checks on a text (free)', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'file contents' }, path: { type: 'string', description: 'optional file name for context' } }, required: ['text'] } },
56
+ { name: 'check_file', description: S.name + ' - run all checks on one file by path (free)', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
57
+ { name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; the full run is free for 7 days)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
58
+ ];
59
+ const result = (id, rows) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: render(rows, 'text') }], structuredContent: { tool: S.name, rules: RULE_N, files: rows } } });
60
+ const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
61
+ rl.on('line', async (line) => {
62
+ let m; try { m = JSON.parse(line); } catch (e) { return; }
63
+ const id = m.id, method = m.method;
64
+ if (method === 'initialize') return send({ jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: '@readystack/' + S.bin, version: '1.0.0' } } });
65
+ if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
66
+ if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
67
+ if (method === 'tools/call') {
68
+ const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
69
+ try {
70
+ if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
71
+ if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
72
+ if (name === 'check_dir') {
73
+ const r = await lic.ensure();
74
+ if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once, 7-day refund): ' + lic.BUY_URL);
75
+ const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
76
+ return result(id, files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t, f) }; }));
77
+ }
78
+ return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
79
+ } catch (e) { return fail(id, String(e && e.message || e)); }
80
+ }
81
+ if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
82
+ });
83
+ }
41
84
  function help() {
42
85
  return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
43
86
  ' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
44
87
  ' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
45
88
  ' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
46
89
  ' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
47
- ' ' + S.bin + ' --rules list the ' + RULE_N + ' rules', '',
90
+ ' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
91
+ ' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
48
92
  'Free: ' + S.free, 'Licence ($' + S.price + ', once, 7-day refund): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
49
93
  }
50
94
  (async function main() {
51
95
  try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { if (Array.isArray(ENGINE.RULES)) for (const r of _feed.rules) ENGINE.RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
52
96
  const a = process.argv.slice(2);
53
97
  const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
98
+ if (a.includes('--mcp')) { mcpServe(); return; }
54
99
  if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
55
100
  if (a.includes('--rules')) { process.stdout.write((RULE_LIST.length ? RULE_LIST.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + (r.message || r.msg || r.id || '')) : Array.from({ length: RULE_N }, (_, i) => String(i + 1).padStart(3) + ' [engine] check ' + (i + 1))).join('\n') + '\n'); return; }
56
101
  if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
@@ -59,7 +104,11 @@ function help() {
59
104
  const paid = !!(dir || fmt || ci);
60
105
  if (paid) {
61
106
  const r = await lic.ensure();
62
- if (!r.ok) { process.stderr.write(S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ', once): ' + lic.BUY_URL + '\n'); process.exit(2); }
107
+ if (!r.ok) {
108
+ const t = trialState(); // s144 reverse trial: the full run is free for 7 days from the first paid use, then the key
109
+ if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once (7-day refund). Get a licence: ' + lic.BUY_URL + '\n');
110
+ else { process.stderr.write(S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ', once): ' + lic.BUY_URL + '\n'); process.exit(2); }
111
+ }
63
112
  }
64
113
  const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
65
114
  if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readystack/email-footer-law-lint",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Fourteen checks on an HTML email footer: CAN-SPAM postal address and 10-business-day opt-out, CASL's 60-day window, and German 5 DDG, the statute that replaced 5 TMG on 2024-05-14.",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
@@ -25,6 +25,12 @@
25
25
  "engines": {
26
26
  "node": ">=18"
27
27
  },
28
+ "mcpName": "io.github.jmshinhwa/email-footer-law-lint",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/jmshinhwa/readystack-themes.git",
32
+ "directory": "email-footer-law-lint"
33
+ },
28
34
  "files": [
29
35
  "cli.js",
30
36
  "license.js",