@readystack/green-claim-lint-empco 1.0.2 → 1.0.4
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.
- package/README.md +2 -2
- package/README.md.bak_s158refund +52 -0
- package/cli.js +52 -11
- package/license.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
|
33
33
|
{ "mcpServers": { "green-claim-lint-empco": { "command": "npx", "args": ["-y", "@readystack/green-claim-lint-empco", "--mcp"] } } }
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Tools: `check_text` and `check_file` (free) · `check_dir` (licence
|
|
36
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
37
37
|
|
|
38
38
|
## Use in CI
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@ Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sw
|
|
|
44
44
|
|
|
45
45
|
(container: `docker run --rm -v "$PWD:/work" getreadystack/green-claim-lint-empco --dir /work --ci`)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
48
48
|
|
|
49
49
|
[Get a licence](https://buy.polar.sh/polar_cl_2e1YKPUdH5MqiTNOPxNdoIn9RO4jK6SCn9fKb3VYDmc)
|
|
50
50
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Green Claim Lint - EU EmpCo 2026
|
|
2
|
+
|
|
3
|
+
Reads your HTML and Markdown product copy and flags the environmental claims Directive (EU) 2024/825 turns into banned practices on 27 September 2026, with the clause and a compliant rewrite for each hit.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx @readystack/green-claim-lint-empco file
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node 18+. The same 13 rules as the VS Code extension, from a terminal or CI.
|
|
12
|
+
|
|
13
|
+
## Free
|
|
14
|
+
|
|
15
|
+
- Open a product page in VS Code or paste it into the free web version and get every banned-claim hit with its Directive (EU) 2024/825 clause, the exact phrase, and a compliant rewrite - all 13 rules, no key, no limit on how many pages you open one at a time.
|
|
16
|
+
- `--rules` lists every rule
|
|
17
|
+
|
|
18
|
+
## With a licence ($29 once)
|
|
19
|
+
|
|
20
|
+
- Scan the entire workspace in one command and export a dated evidence report (CSV plus Markdown) that lists every page, hit, clause and rewrite - a file you keep and hand to counsel, a marketplace or an auditor.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
@readystack/green-claim-lint-empco --dir ./templates --report html --out report.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
UCPD Art. 13(3): for widespread infringements Member States must provide fines of at least 4% of the trader's annual turnover in the Member States concerned, or at least EUR 2,000,000 where turnover information is unavailable.
|
|
27
|
+
|
|
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": { "green-claim-lint-empco": { "command": "npx", "args": ["-y", "@readystack/green-claim-lint-empco", "--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: Green Claim Lint - EU EmpCo 2026
|
|
42
|
+
run: npx -y @readystack/green-claim-lint-empco --dir . --ci
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/green-claim-lint-empco --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.
|
|
48
|
+
|
|
49
|
+
[Get a licence](https://buy.polar.sh/polar_cl_2e1YKPUdH5MqiTNOPxNdoIn9RO4jK6SCn9fKb3VYDmc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
<!-- green claim lint empco -->
|
package/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024
|
|
|
16
16
|
function walk(dir, exts, out) {
|
|
17
17
|
let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
|
|
18
18
|
for (const e of ents) {
|
|
19
|
-
if (
|
|
19
|
+
if (['node_modules', '.git', '.hg', '.svn', '.venv', 'venv', '.tox', '.cache', '.next', '.nuxt', '.terraform', '__pycache__'].includes(e.name)) continue; // s158 — .github · .gitlab-ci.yml · .env · .well-known 은 본다 (옛 줄은 점으로 시작하는 것을 전부 건너뛰어 워크플로 검사기가 .github/workflows 를 못 봤다)
|
|
20
20
|
const p = path.join(dir, e.name);
|
|
21
21
|
if (e.isDirectory()) walk(p, exts, out);
|
|
22
22
|
else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
|
|
@@ -39,22 +39,55 @@ function render(rows, fmt) {
|
|
|
39
39
|
return o;
|
|
40
40
|
}
|
|
41
41
|
function trialState() {
|
|
42
|
+
// s158 — ⚑7일 무료 없음: 새 체험은 ⛔열지 않는다 · 이미 시작된 체험(trial.json)만 끝까지 지킨다 (읽기 전용)
|
|
42
43
|
if (process.env.READYSTACK_NO_TRIAL) return { active: false };
|
|
43
44
|
try {
|
|
44
45
|
const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return { active: new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t.until };
|
|
46
|
+
const t = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
47
|
+
return { active: !!(t && t.until) && new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t && t.until };
|
|
48
48
|
} catch (e) { return { active: false }; }
|
|
49
49
|
}
|
|
50
|
+
// ★s158 2026-09-23 — the free run SPEAKS about the rest of the folder (vsix: auto.js · crx: welcome tab).
|
|
51
|
+
// The answer for the named files is complete and free; the next step is shown with the user's OWN numbers:
|
|
52
|
+
// "this folder has N more matching files - M issues in K of them" + the one command that sweeps them.
|
|
53
|
+
// Never blocks, never starts a trial (s158: no free trial - the free answer on the named files is the "try it"), human seat only, 400 files / 1.5 s cap.
|
|
54
|
+
function folderHint(done) {
|
|
55
|
+
if (!process.stderr.isTTY || process.env.CI || process.env.READYSTACK_NO_HINT || !done.length) return;
|
|
56
|
+
const base = path.dirname(path.resolve(done[0]));
|
|
57
|
+
const seen = new Set(done.map((f) => path.resolve(f)));
|
|
58
|
+
const all = walk(base, S.exts || [], []).filter((f) => !seen.has(path.resolve(f)));
|
|
59
|
+
if (!all.length) return;
|
|
60
|
+
const t0 = Date.now(); let issues = 0, hitFiles = 0, scanned = 0;
|
|
61
|
+
for (const f of all.slice(0, 400)) { if (Date.now() - t0 > 1500) break; let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } scanned++; const h = scan(t, f); if (h.length) { issues += h.length; hitFiles++; } }
|
|
62
|
+
const rel = path.relative(process.cwd(), base) || '.';
|
|
63
|
+
const tail = trialState().active ? 'free during your trial' : '$' + S.price + ' once';
|
|
64
|
+
const more = all.length + ' more matching file' + (all.length > 1 ? 's' : '');
|
|
65
|
+
const found = issues ? ' - ' + (scanned < all.length ? 'at least ' : '') + issues + ' issue' + (issues > 1 ? 's' : '') + ' in ' + hitFiles + ' of them' : ' - no issues found in them';
|
|
66
|
+
process.stderr.write('\n' + (rel === '.' ? 'This folder' : rel) + ' has ' + more + found + '.\n' + (issues ? 'Sweep them all: ' + S.bin + ' --dir ' + (/\s/.test(rel) ? JSON.stringify(rel) : rel) + ' (' + tail + ')\n' : ''));
|
|
67
|
+
}
|
|
68
|
+
let _usePinged = false;
|
|
69
|
+
function pingUse() {
|
|
70
|
+
try {
|
|
71
|
+
if (_usePinged) return; _usePinged = true;
|
|
72
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return;
|
|
73
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY)) return; // human seat only (s152)
|
|
74
|
+
const https = require('https');
|
|
75
|
+
const body = JSON.stringify({ t: 'use', slug: S.bin, src: 'cli', why: 'free' });
|
|
76
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
77
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + S.bin } }, function (res) { res.resume(); });
|
|
78
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
79
|
+
req.write(body); req.end();
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
}
|
|
50
82
|
function mcpServe() {
|
|
51
|
-
|
|
83
|
+
process.env.READYSTACK_MCP = '1'; // s152 - 에이전트(Claude Code·Cursor)가 부른 세션은 사람 자리다 · 키 판 핑 src=mcp
|
|
84
|
+
// s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (s158: no free trial): check_dir.
|
|
52
85
|
const rl = require('readline').createInterface({ input: process.stdin });
|
|
53
86
|
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
54
87
|
const tools = [
|
|
55
88
|
{ 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
89
|
{ 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;
|
|
90
|
+
{ name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; $' + S.price + ' once)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
|
|
58
91
|
];
|
|
59
92
|
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
93
|
const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
|
|
@@ -71,7 +104,7 @@ function mcpServe() {
|
|
|
71
104
|
if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
|
|
72
105
|
if (name === 'check_dir') {
|
|
73
106
|
const r = await lic.ensure();
|
|
74
|
-
if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once
|
|
107
|
+
if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once): ' + lic.BUY_URL);
|
|
75
108
|
const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
|
|
76
109
|
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
110
|
}
|
|
@@ -89,7 +122,7 @@ function help() {
|
|
|
89
122
|
' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
90
123
|
' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
|
|
91
124
|
' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
|
|
92
|
-
'Free: ' + S.free, 'Licence ($' + S.price + ', once
|
|
125
|
+
'Free: ' + S.free, 'Licence ($' + S.price + ', once): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
|
|
93
126
|
}
|
|
94
127
|
(async function main() {
|
|
95
128
|
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 구독 피드 병합 (키 있는 손님만)
|
|
@@ -105,9 +138,16 @@ function help() {
|
|
|
105
138
|
if (paid) {
|
|
106
139
|
const r = await lic.ensure();
|
|
107
140
|
if (!r.ok) {
|
|
108
|
-
const t = trialState(); //
|
|
109
|
-
if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once
|
|
110
|
-
else {
|
|
141
|
+
const t = trialState(); // s158 — ⚑7일 무료 없음 · 이미 시작된 체험만 지킨다
|
|
142
|
+
if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once. Get a licence: ' + lic.BUY_URL + '\n');
|
|
143
|
+
else {
|
|
144
|
+
// s158 — the key is asked WITH the customer's own count (endowment · open loop): how many issues this folder holds.
|
|
145
|
+
let own = '';
|
|
146
|
+
if (dir) { try { const all = walk(dir, exts, []); let n = 0, k = 0, sc = 0; const t0 = Date.now();
|
|
147
|
+
for (const f of all.slice(0, 2000)) { if (Date.now() - t0 > 4000) break; let tx = ''; try { tx = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } sc++; const h = scan(tx, f); if (h.length) { n += h.length; k++; } }
|
|
148
|
+
if (n) own = dir + ': ' + (sc < all.length ? 'at least ' : '') + n + ' issue' + (n > 1 ? 's' : '') + ' in ' + k + ' of ' + all.length + ' files.\n'; } catch (e) {} }
|
|
149
|
+
process.stderr.write(own + 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);
|
|
150
|
+
}
|
|
111
151
|
}
|
|
112
152
|
}
|
|
113
153
|
const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
|
|
@@ -115,6 +155,7 @@ function help() {
|
|
|
115
155
|
const rows = 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) }; });
|
|
116
156
|
const text = render(rows, fmt || 'text');
|
|
117
157
|
if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
|
|
158
|
+
if (!paid) { try { folderHint(files); } catch (e) {} pingUse(); } // s158 — a free run ends with the user's own folder count + one anonymous 'used' count (human seat only)
|
|
118
159
|
const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
|
|
119
160
|
if (ci && errors) process.exit(1);
|
|
120
161
|
})().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
|
package/license.js
CHANGED
|
@@ -37,8 +37,9 @@ let _pinged = false;
|
|
|
37
37
|
function pingPaywall(why) {
|
|
38
38
|
try {
|
|
39
39
|
if (_pinged) return; _pinged = true;
|
|
40
|
-
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY) return;
|
|
41
|
-
|
|
40
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return; // s151: CI(깃허브 액션 등)와 우리 빌드는 손님이 아니다
|
|
41
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY || process.env.READYSTACK_MCP === '1')) return; // s152: 사람 자리(터미널·MCP 세션)에서만 센다 - 발행 1분 뒤 남의 실행기(JP · 우리 3대는 US)가 돌린 no_key 13건은 손님이 아니다
|
|
42
|
+
const body = JSON.stringify({ t: 'paywall', slug: SLUG, src: process.env.READYSTACK_MCP === '1' ? 'mcp' : 'cli', why: why || 'no_key' });
|
|
42
43
|
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
43
44
|
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
|
|
44
45
|
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@readystack/green-claim-lint-empco",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Reads your HTML and Markdown product copy and flags the environmental claims Directive (EU) 2024/825 turns into banned practices on 27 September 2026, with the clause and a compliant rewrite for each ",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"publishConfig": {
|