readystack-ai-model-license-lint 1.0.5__tar.gz
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.
- readystack_ai_model_license_lint-1.0.5/LICENSE.txt +15 -0
- readystack_ai_model_license_lint-1.0.5/PKG-INFO +82 -0
- readystack_ai_model_license_lint-1.0.5/README.md +62 -0
- readystack_ai_model_license_lint-1.0.5/pyproject.toml +29 -0
- readystack_ai_model_license_lint-1.0.5/setup.cfg +4 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/__init__.py +1 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/__main__.py +12 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/js/cli.js +161 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/js/engine.js +72 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/js/license.js +83 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/js/package.json +42 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/js/rules.json +182 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint/js/strings.json +10 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/PKG-INFO +82 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/SOURCES.txt +17 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/dependency_links.txt +1 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/entry_points.txt +2 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/requires.txt +1 -0
- readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
AI Model License Lint — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Scans the file or workspace you have open and names, for every open-weight model ID it finds, the exact licence clause that binds you and the case that breaks it. is free for any use, personal or commercial, for as long as you keep the
|
|
5
|
+
extension installed, and the online check at the product page is free without registration.
|
|
6
|
+
|
|
7
|
+
Paid scope
|
|
8
|
+
Exports the evidence you can hand to procurement or counsel: a generated NOTICE/attribution file and a dated per-model obligation report for the whole workspace, plus a CI exit code. is the paid part. It asks for a licence key issued at purchase (one seat, one key). The key is checked against the payment provider and
|
|
9
|
+
cached locally for 30 days so it keeps working offline.
|
|
10
|
+
|
|
11
|
+
Redistribution
|
|
12
|
+
You may not resell, re-host or bundle this extension or its rule set. The rule set and the
|
|
13
|
+
engine are provided as-is; check results are advisory and do not constitute legal advice.
|
|
14
|
+
|
|
15
|
+
(c) ReadyStack — https://getreadystack.com
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readystack-ai-model-license-lint
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture (bundled Node.js runtime)
|
|
5
|
+
Author-email: ReadyStack <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/ai-model-license-lint?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/ai-model-license-lint?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: llama license,model license,open weights,compliance,gemma terms,flux dev non-commercial,ai act,attribution
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: nodejs-wheel-binaries>=20
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# AI Model License Lint
|
|
22
|
+
|
|
23
|
+
> Runs the same checker as the npm package `@readystack/ai-model-license-lint` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
|
|
24
|
+
|
|
25
|
+
Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
ai-model-license-lint file
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Node 18+. The same 22 rules as the VS Code extension, from a terminal or CI.
|
|
34
|
+
|
|
35
|
+
## Free
|
|
36
|
+
|
|
37
|
+
- Scans the file you have open and names, for every open-weight model identifier, the exact licence clause that binds you and the case that breaks it - 22 rules, in VS Code and in the browser, no account. On the bundled 33-line fixture it returns 17 findings, 5 of them blocking, naming 7 licences; on the clean fixture it returns 0.
|
|
38
|
+
- `--rules` lists every rule
|
|
39
|
+
|
|
40
|
+
## With a licence ($29 once)
|
|
41
|
+
|
|
42
|
+
- Exports the evidence: a generated NOTICE / attribution file and a dated per-model obligation report for the whole workspace, in the form procurement and counsel ask for, plus a CI exit code so a new model ID cannot land unreviewed.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
@readystack/ai-model-license-lint --dir ./templates --report html --out report.html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Outside technology counsel bills roughly USD 350-600 per hour for an open-source and model-licence review, repeated each time the model list changes
|
|
49
|
+
|
|
50
|
+
## Use from an AI agent (MCP)
|
|
51
|
+
|
|
52
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{ "mcpServers": { "ai-model-license-lint": { "command": "npx", "args": ["-y", "@readystack/ai-model-license-lint", "--mcp"] } } }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
59
|
+
|
|
60
|
+
## Use in CI
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
- name: AI Model License Lint
|
|
64
|
+
run: npx -y @readystack/ai-model-license-lint --dir . --ci
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/ai-model-license-lint --dir /work --ci`)
|
|
68
|
+
|
|
69
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
70
|
+
|
|
71
|
+
[Get a licence](https://buy.polar.sh/polar_cl_n48nKSGuBUvKyot5tlEi3ACu4uVv6EJrVAOvB2JgLDv)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
<!-- ai model license lint -->
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Install (PyPI)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
pip install readystack-ai-model-license-lint
|
|
81
|
+
ai-model-license-lint --help
|
|
82
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# AI Model License Lint
|
|
2
|
+
|
|
3
|
+
> Runs the same checker as the npm package `@readystack/ai-model-license-lint` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
|
|
4
|
+
|
|
5
|
+
Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
ai-model-license-lint file
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node 18+. The same 22 rules as the VS Code extension, from a terminal or CI.
|
|
14
|
+
|
|
15
|
+
## Free
|
|
16
|
+
|
|
17
|
+
- Scans the file you have open and names, for every open-weight model identifier, the exact licence clause that binds you and the case that breaks it - 22 rules, in VS Code and in the browser, no account. On the bundled 33-line fixture it returns 17 findings, 5 of them blocking, naming 7 licences; on the clean fixture it returns 0.
|
|
18
|
+
- `--rules` lists every rule
|
|
19
|
+
|
|
20
|
+
## With a licence ($29 once)
|
|
21
|
+
|
|
22
|
+
- Exports the evidence: a generated NOTICE / attribution file and a dated per-model obligation report for the whole workspace, in the form procurement and counsel ask for, plus a CI exit code so a new model ID cannot land unreviewed.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
@readystack/ai-model-license-lint --dir ./templates --report html --out report.html
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Outside technology counsel bills roughly USD 350-600 per hour for an open-source and model-licence review, repeated each time the model list changes
|
|
29
|
+
|
|
30
|
+
## Use from an AI agent (MCP)
|
|
31
|
+
|
|
32
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{ "mcpServers": { "ai-model-license-lint": { "command": "npx", "args": ["-y", "@readystack/ai-model-license-lint", "--mcp"] } } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
39
|
+
|
|
40
|
+
## Use in CI
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: AI Model License Lint
|
|
44
|
+
run: npx -y @readystack/ai-model-license-lint --dir . --ci
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/ai-model-license-lint --dir /work --ci`)
|
|
48
|
+
|
|
49
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
50
|
+
|
|
51
|
+
[Get a licence](https://buy.polar.sh/polar_cl_n48nKSGuBUvKyot5tlEi3ACu4uVv6EJrVAOvB2JgLDv)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<!-- ai model license lint -->
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Install (PyPI)
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
pip install readystack-ai-model-license-lint
|
|
61
|
+
ai-model-license-lint --help
|
|
62
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "readystack-ai-model-license-lint"
|
|
7
|
+
version = "1.0.5"
|
|
8
|
+
description = "Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture (bundled Node.js runtime)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["nodejs-wheel-binaries>=20"]
|
|
12
|
+
license = {text = "Proprietary - see LICENSE.txt"}
|
|
13
|
+
keywords = ["llama license", "model license", "open weights", "compliance", "gemma terms", "flux dev non-commercial", "ai act", "attribution"]
|
|
14
|
+
authors = [{name = "ReadyStack", email = "support@getreadystack.com"}]
|
|
15
|
+
classifiers = ["Programming Language :: Python :: 3", "Environment :: Console", "Topic :: Software Development :: Quality Assurance", "Operating System :: OS Independent"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://getreadystack.com/tools/ai-model-license-lint?ref=pypi"
|
|
19
|
+
Documentation = "https://getreadystack.com/tools/ai-model-license-lint?ref=pypi"
|
|
20
|
+
Support = "https://getreadystack.com/support"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
"ai-model-license-lint" = "readystack_ai_model_license_lint.__main__:main"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
"readystack_ai_model_license_lint" = ["js/*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.5'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os, sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
# Runs the same checker as the npm package on the Node.js runtime installed with this package (nodejs-wheel-binaries).
|
|
6
|
+
from nodejs_wheel import node
|
|
7
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
return node([os.path.join(here, "js", "cli.js")] + sys.argv[1:])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
sys.exit(main())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs'), path = require('path');
|
|
5
|
+
const RULES = require('./rules.json');
|
|
6
|
+
const S = require('./strings.json');
|
|
7
|
+
const lic = require('./license.js');
|
|
8
|
+
const ENGINE = require('./engine.js'); // s144 — scaffold products: the same engine the VS Code extension runs
|
|
9
|
+
const RULE_LIST = Array.isArray(ENGINE.RULES) ? ENGINE.RULES : (Array.isArray(RULES) ? RULES : []);
|
|
10
|
+
const RULE_N = ENGINE.RULE_COUNT || RULE_LIST.length;
|
|
11
|
+
function scan(text, file) {
|
|
12
|
+
const r = ENGINE.engine.check(String(text), { today: new Date().toISOString().slice(0, 10), path: file || '' });
|
|
13
|
+
return (r && r.findings || []).map((f) => ({ line: parseInt(f.line, 10) || 1, msg: String(f.msg || f.message || f.check || ''), fix: f.fix || null, sev: (String(f.sev || 'error').toLowerCase().startsWith('err') ? 'error' : 'warn') }));
|
|
14
|
+
}
|
|
15
|
+
function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
|
|
16
|
+
function walk(dir, exts, out) {
|
|
17
|
+
let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
|
|
18
|
+
for (const e of ents) {
|
|
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
|
+
const p = path.join(dir, e.name);
|
|
21
|
+
if (e.isDirectory()) walk(p, exts, out);
|
|
22
|
+
else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
27
|
+
function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
|
|
28
|
+
function render(rows, fmt) {
|
|
29
|
+
if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULE_N, files: rows }, null, 1);
|
|
30
|
+
if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
|
|
31
|
+
if (fmt === 'html') {
|
|
32
|
+
const n = rows.reduce((a, r) => a + r.hits.length, 0);
|
|
33
|
+
let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
|
|
34
|
+
o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULE_N + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
|
|
35
|
+
for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
|
|
36
|
+
return o + '</table>';
|
|
37
|
+
}
|
|
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
|
+
return o;
|
|
40
|
+
}
|
|
41
|
+
function trialState() {
|
|
42
|
+
// s158 — ⚑7일 무료 없음: 새 체험은 ⛔열지 않는다 · 이미 시작된 체험(trial.json)만 끝까지 지킨다 (읽기 전용)
|
|
43
|
+
if (process.env.READYSTACK_NO_TRIAL) return { active: false };
|
|
44
|
+
try {
|
|
45
|
+
const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
|
|
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
|
+
} catch (e) { return { active: false }; }
|
|
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
|
+
}
|
|
82
|
+
function mcpServe() {
|
|
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.
|
|
85
|
+
const rl = require('readline').createInterface({ input: process.stdin });
|
|
86
|
+
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
87
|
+
const tools = [
|
|
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'] } },
|
|
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'] } },
|
|
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'] } }
|
|
91
|
+
];
|
|
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 } } });
|
|
93
|
+
const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
|
|
94
|
+
rl.on('line', async (line) => {
|
|
95
|
+
let m; try { m = JSON.parse(line); } catch (e) { return; }
|
|
96
|
+
const id = m.id, method = m.method;
|
|
97
|
+
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' } } });
|
|
98
|
+
if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
|
|
99
|
+
if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
|
|
100
|
+
if (method === 'tools/call') {
|
|
101
|
+
const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
|
|
102
|
+
try {
|
|
103
|
+
if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
|
|
104
|
+
if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
|
|
105
|
+
if (name === 'check_dir') {
|
|
106
|
+
const r = await lic.ensure();
|
|
107
|
+
if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once): ' + lic.BUY_URL);
|
|
108
|
+
const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
|
|
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) }; }));
|
|
110
|
+
}
|
|
111
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
|
|
112
|
+
} catch (e) { return fail(id, String(e && e.message || e)); }
|
|
113
|
+
}
|
|
114
|
+
if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function help() {
|
|
118
|
+
return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
|
|
119
|
+
' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
|
|
120
|
+
' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
|
|
121
|
+
' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
|
|
122
|
+
' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
123
|
+
' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
|
|
124
|
+
' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
|
|
125
|
+
'Free: ' + S.free, 'Licence ($' + S.price + ', once): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
|
|
126
|
+
}
|
|
127
|
+
(async function main() {
|
|
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 구독 피드 병합 (키 있는 손님만)
|
|
129
|
+
const a = process.argv.slice(2);
|
|
130
|
+
const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
|
|
131
|
+
if (a.includes('--mcp')) { mcpServe(); return; }
|
|
132
|
+
if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
|
|
133
|
+
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; }
|
|
134
|
+
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); }
|
|
135
|
+
const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
|
|
136
|
+
const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
|
|
137
|
+
const paid = !!(dir || fmt || ci);
|
|
138
|
+
if (paid) {
|
|
139
|
+
const r = await lic.ensure();
|
|
140
|
+
if (!r.ok) {
|
|
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
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
|
|
154
|
+
if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
|
|
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) }; });
|
|
156
|
+
const text = render(rows, fmt || 'text');
|
|
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)
|
|
159
|
+
const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
|
|
160
|
+
if (ci && errors) process.exit(1);
|
|
161
|
+
})().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* AI Model License Lint - engine.
|
|
2
|
+
One brain, two homes: require()d by the VS Code extension, and read off
|
|
3
|
+
window.MLL_RULES by the single-page web edition. */
|
|
4
|
+
(function (root) {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
var DATA = (typeof module !== 'undefined' && module.exports)
|
|
8
|
+
? require('./rules.json')
|
|
9
|
+
: root.MLL_RULES;
|
|
10
|
+
|
|
11
|
+
var RULES = Array.isArray(DATA) ? DATA : ((DATA && DATA.rules) || []);
|
|
12
|
+
var RULES_VERSION = '2026-09-14';
|
|
13
|
+
|
|
14
|
+
var cache = {};
|
|
15
|
+
function rx(src, flags) {
|
|
16
|
+
var key = flags + ' ' + src;
|
|
17
|
+
if (!cache[key]) cache[key] = new RegExp(src, flags);
|
|
18
|
+
return cache[key];
|
|
19
|
+
}
|
|
20
|
+
function hit(src, text) { return !!src && rx(src, 'i').test(text); }
|
|
21
|
+
|
|
22
|
+
/* A model id pasted into a docstring or a README is still the identifier
|
|
23
|
+
procurement reads, so comment lines count. Blank lines never do. */
|
|
24
|
+
function check(text, opts) {
|
|
25
|
+
opts = opts || {};
|
|
26
|
+
text = String(text == null ? '' : text);
|
|
27
|
+
var lines = text.split(/\r?\n/);
|
|
28
|
+
var findings = [];
|
|
29
|
+
|
|
30
|
+
for (var r = 0; r < RULES.length; r++) {
|
|
31
|
+
var rule = RULES[r];
|
|
32
|
+
if (rule.needs_file && !hit(rule.needs_file, text)) continue;
|
|
33
|
+
if (rule.not_file && hit(rule.not_file, text)) continue;
|
|
34
|
+
if (!rule.re) continue;
|
|
35
|
+
|
|
36
|
+
var re = rx(rule.re, 'i');
|
|
37
|
+
|
|
38
|
+
for (var i = 0; i < lines.length; i++) {
|
|
39
|
+
var line = lines[i];
|
|
40
|
+
if (!line.trim()) continue;
|
|
41
|
+
if (!re.test(line)) continue;
|
|
42
|
+
if (rule.not_line && hit(rule.not_line, line)) continue;
|
|
43
|
+
|
|
44
|
+
findings.push({
|
|
45
|
+
check: rule.id,
|
|
46
|
+
sev: rule.sev || 'warn',
|
|
47
|
+
msg: rule.msg,
|
|
48
|
+
line: i + 1
|
|
49
|
+
});
|
|
50
|
+
if (rule.once || rule.scope === 'file') break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
findings.sort(function (a, b) { return a.line - b.line; });
|
|
55
|
+
return {
|
|
56
|
+
findings: findings,
|
|
57
|
+
rules_version: RULES_VERSION,
|
|
58
|
+
today: opts.today || ''
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var API = {
|
|
63
|
+
engine: { check: check },
|
|
64
|
+
check: check,
|
|
65
|
+
RULES: RULES,
|
|
66
|
+
RULE_COUNT: RULES.length,
|
|
67
|
+
RULES_VERSION: RULES_VERSION
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
root.MLLENGINE = API;
|
|
71
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = API;
|
|
72
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
|
|
2
|
+
'use strict';
|
|
3
|
+
const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
|
|
4
|
+
const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
|
|
5
|
+
const BENEFIT_ID = '4bb66fea-868b-4838-9634-32da8f8f0093'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
|
|
6
|
+
const BUY_URL = 'https://buy.polar.sh/polar_cl_n48nKSGuBUvKyot5tlEi3ACu4uVv6EJrVAOvB2JgLDv';
|
|
7
|
+
const SLUG = 'ai-model-license-lint';
|
|
8
|
+
const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
|
|
9
|
+
const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
|
|
10
|
+
function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
|
|
11
|
+
function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
|
|
12
|
+
function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
|
|
13
|
+
const ALL_BENEFIT_ID = '22692551-5203-4467-b1a3-e33cdba6589d'; // s149 2026-09-17 — 팀 키(전 린터 한 키 · Polar benefit) · 상품 benefit 다음에 한 번 더 묻는다
|
|
14
|
+
function validate(key) {
|
|
15
|
+
return validate1(key, BENEFIT_ID).then(function (r) { return (r.ok || r.offline || !/^[0-9a-f-]{36}$/.test(ALL_BENEFIT_ID)) ? r : validate1(key, ALL_BENEFIT_ID); });
|
|
16
|
+
}
|
|
17
|
+
function validate1(key, ben) {
|
|
18
|
+
return new Promise(function (resolve) {
|
|
19
|
+
if (!ORG_ID) return resolve({ ok: false, offline: false });
|
|
20
|
+
const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(ben) ? { key: key, organization_id: ORG_ID, benefit_id: ben } : { key: key, organization_id: ORG_ID });
|
|
21
|
+
const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
|
|
22
|
+
headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
|
|
23
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
24
|
+
res.on('end', function () {
|
|
25
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
|
|
26
|
+
try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
|
|
27
|
+
catch (e) { resolve({ ok: false, offline: false }); }
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
31
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
32
|
+
req.write(body); req.end();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// ★s151 2026-09-19 — 키 판(키가 없거나 거절된 순간)을 익명으로 센다 (슬러그·출처·이유만) · DO_NOT_TRACK=1 · READYSTACK_NO_TELEMETRY 면 안 보낸다 · 실패는 조용히 · 프로세스당 한 번.
|
|
36
|
+
let _pinged = false;
|
|
37
|
+
function pingPaywall(why) {
|
|
38
|
+
try {
|
|
39
|
+
if (_pinged) return; _pinged = true;
|
|
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' });
|
|
43
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
44
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
|
|
45
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
46
|
+
req.write(body); req.end();
|
|
47
|
+
} catch (e) { /* 세는 것이 실패해도 상품은 돈다 */ }
|
|
48
|
+
}
|
|
49
|
+
async function ensure(explicitKey) {
|
|
50
|
+
const st = load();
|
|
51
|
+
const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
|
|
52
|
+
if (!key) { pingPaywall('no_key'); return { ok: false, why: 'no_key' }; } // s151
|
|
53
|
+
const age = Date.now() - (st.okAt || 0);
|
|
54
|
+
if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
|
|
55
|
+
const r = await validate(String(key).trim());
|
|
56
|
+
if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
|
|
57
|
+
if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
|
|
58
|
+
if (!r.offline) pingPaywall('invalid'); // s151
|
|
59
|
+
return { ok: false, why: r.offline ? 'offline' : 'invalid' };
|
|
60
|
+
}
|
|
61
|
+
// ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
|
|
62
|
+
const FEED_URL = 'https://getreadystack.com/api/rules/';
|
|
63
|
+
function pullFeed() {
|
|
64
|
+
const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
|
|
65
|
+
if (!key) return Promise.resolve(cached);
|
|
66
|
+
if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
|
|
67
|
+
return new Promise(function (resolve) {
|
|
68
|
+
let req;
|
|
69
|
+
try {
|
|
70
|
+
req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
|
|
71
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
72
|
+
res.on('end', function () {
|
|
73
|
+
if (res.statusCode !== 200) return resolve(cached);
|
|
74
|
+
try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
|
|
75
|
+
catch (e) { resolve(cached); }
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
} catch (e) { return resolve(cached); }
|
|
79
|
+
req.on('timeout', function () { req.destroy(); resolve(cached); });
|
|
80
|
+
req.on('error', function () { resolve(cached); });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
module.exports = { ensure, BUY_URL, storePath, pullFeed };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/ai-model-license-lint",
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"llama license",
|
|
11
|
+
"model license",
|
|
12
|
+
"open weights",
|
|
13
|
+
"compliance",
|
|
14
|
+
"gemma terms",
|
|
15
|
+
"flux dev non-commercial",
|
|
16
|
+
"ai act",
|
|
17
|
+
"attribution"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://getreadystack.com",
|
|
20
|
+
"funding": "https://buy.polar.sh/polar_cl_n48nKSGuBUvKyot5tlEi3ACu4uVv6EJrVAOvB2JgLDv",
|
|
21
|
+
"bin": {
|
|
22
|
+
"ai-model-license-lint": "cli.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"mcpName": "io.github.jmshinhwa/ai-model-license-lint",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
31
|
+
"directory": "ai-model-license-lint"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"cli.js",
|
|
35
|
+
"license.js",
|
|
36
|
+
"rules.json",
|
|
37
|
+
"strings.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE.txt",
|
|
40
|
+
"engine.js"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "llama32-multimodal-eu-domicile",
|
|
4
|
+
"sev": "error",
|
|
5
|
+
"scope": "line",
|
|
6
|
+
"once": true,
|
|
7
|
+
"re": "llama[-_ ]?3[._]2[-_ ]?(?:11b|90b)|llama[-_ ]?3[._]2[^\\n]{0,30}vision",
|
|
8
|
+
"msg": "Llama 3.2 multimodal (11B/90B Vision): the Llama 3.2 Community License withholds the grant from individuals domiciled in, and companies with a principal place of business in, the European Union. An EU-domiciled team has no licence at all here - not a narrower one. Use a text-only Llama build, or a hosted provider that holds its own licence."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "llama4-multimodal-eu-domicile",
|
|
12
|
+
"sev": "error",
|
|
13
|
+
"scope": "line",
|
|
14
|
+
"once": true,
|
|
15
|
+
"re": "llama[-_ ]?4[-_ ]?(?:scout|maverick)",
|
|
16
|
+
"msg": "Llama 4 Scout and Maverick are multimodal models, and the Llama 4 Community License carries the same EU principal-place-of-business exclusion for multimodal use. Confirm where your company is established before these weights reach a build."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "llama-700m-mau-cap",
|
|
20
|
+
"sev": "warn",
|
|
21
|
+
"scope": "line",
|
|
22
|
+
"once": true,
|
|
23
|
+
"re": "meta-llama/|llama[-_ ]?[234][-_ .]",
|
|
24
|
+
"msg": "Llama Community License: if the product had more than 700 million monthly active users in the calendar month before the release, the grant does not extend to you and you must request a separate licence from Meta. Write down the MAU figure you relied on at release date - that is the number an auditor asks for."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "llama-built-with-attribution",
|
|
28
|
+
"sev": "warn",
|
|
29
|
+
"scope": "file",
|
|
30
|
+
"re": "meta-llama/|llama[-_ ]?[234][-_ .]",
|
|
31
|
+
"not_file": "built with llama",
|
|
32
|
+
"msg": "Llama 3.1 and later require the notice \"Built with Llama\" to be displayed prominently on any website, user interface, blog post, about page or product documentation for anything built with these weights or their outputs. That string appears nowhere in this file."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "llama-notice-text-missing",
|
|
36
|
+
"sev": "warn",
|
|
37
|
+
"scope": "file",
|
|
38
|
+
"re": "meta-llama/|llama[-_ ]?[234][-_ .]",
|
|
39
|
+
"not_file": "Community License, Copyright",
|
|
40
|
+
"msg": "The Llama Community License requires a Notice file distributed with the model reading: \"Llama 3.1 is licensed under the Llama 3.1 Community License, Copyright (c) Meta Platforms, Inc. All Rights Reserved.\" with the version you actually ship. No notice text of that form is present."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "llama-derivative-naming",
|
|
44
|
+
"sev": "warn",
|
|
45
|
+
"scope": "line",
|
|
46
|
+
"once": true,
|
|
47
|
+
"re": "\\bpeft\\b|\\blora\\b|fine[-_ ]?tun|adapter_model|merge_and_unload",
|
|
48
|
+
"needs_file": "meta-llama/|llama[-_ ]?[234][-_ .]",
|
|
49
|
+
"msg": "You are deriving a model from Llama weights. The Community License requires the name of any distributed derivative to begin with \"Llama\", and the model card to say it is built with Llama. A fine-tune published under a different brand name breaks the naming term even when everything else is right."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "llama-aup-passthrough",
|
|
53
|
+
"sev": "warn",
|
|
54
|
+
"scope": "file",
|
|
55
|
+
"re": "meta-llama/|llama[-_ ]?[234][-_ .]",
|
|
56
|
+
"not_file": "acceptable use|use_policy|AUP",
|
|
57
|
+
"msg": "The Llama Acceptable Use Policy is incorporated into the licence by reference and must travel to anyone you hand the weights, a derivative or a hosted endpoint. Shipping the licence text alone leaves the restriction unenforced downstream, which is your breach, not theirs."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "gemma-terms-passthrough",
|
|
61
|
+
"sev": "warn",
|
|
62
|
+
"scope": "line",
|
|
63
|
+
"once": true,
|
|
64
|
+
"re": "google/gemma|\\bgemma[-_ ]?[0-9]|codegemma|paligemma|shieldgemma",
|
|
65
|
+
"msg": "Gemma is distributed under the Gemma Terms of Use with prohibited-use restrictions attached. You must pass the same restrictions to every downstream recipient and supply the Terms with any distribution, derivative or hosted endpoint you put in front of the weights."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "gemma-labelled-open-source",
|
|
69
|
+
"sev": "warn",
|
|
70
|
+
"scope": "line",
|
|
71
|
+
"once": true,
|
|
72
|
+
"re": "open[- ]?source|apache[-_ ]?2|MIT licen[cs]e",
|
|
73
|
+
"needs_file": "google/gemma|\\bgemma[-_ ]?[0-9]",
|
|
74
|
+
"msg": "This file describes a Gemma build with an OSI licence label. Gemma is not under an OSI-approved licence: it carries use restrictions the Gemma Terms of Use impose. Calling it open source in a README, a model card or an SBOM is the misstatement a procurement review picks up first."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "flux-dev-non-commercial",
|
|
78
|
+
"sev": "error",
|
|
79
|
+
"scope": "line",
|
|
80
|
+
"once": true,
|
|
81
|
+
"re": "flux[.\\-_ ]?1[-_ ]?\\[?dev",
|
|
82
|
+
"msg": "FLUX.1 [dev] weights are released under the FLUX.1 [dev] Non-Commercial License. Serving them inside anything you charge for - or anything that generates revenue for the company - needs a commercial licence from Black Forest Labs. FLUX.1 [schnell] is Apache 2.0 and is the drop-in that does not."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "flux-pro-weights-not-released",
|
|
86
|
+
"sev": "error",
|
|
87
|
+
"scope": "line",
|
|
88
|
+
"once": true,
|
|
89
|
+
"re": "flux[.\\-_ ]?1[-_ ]?\\[?pro",
|
|
90
|
+
"msg": "FLUX.1 [pro] weights are not publicly released; the model is offered through the API and through licensed partners. A local weights path or a repository download for FLUX.1 [pro] is either the wrong identifier or a copy nobody granted you."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "sd3-revenue-threshold",
|
|
94
|
+
"sev": "warn",
|
|
95
|
+
"scope": "line",
|
|
96
|
+
"once": true,
|
|
97
|
+
"re": "stable[-_ ]?diffusion[-_ ]?3|sd3[._ ]?5|stabilityai/stable-diffusion-3",
|
|
98
|
+
"msg": "Stable Diffusion 3.x is under the Stability AI Community License: research use is free, and commercial use is free only while the organisation stays under USD 1 million in annual revenue. Crossing that line requires an Enterprise licence. It is a revenue trigger, so the day it fires has nothing to do with how much you use the model."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "sd15-openrail-restrictions",
|
|
102
|
+
"sev": "warn",
|
|
103
|
+
"scope": "line",
|
|
104
|
+
"once": true,
|
|
105
|
+
"re": "stable[-_ ]?diffusion[-_ ]?v?1[-_ .]5|runwayml/stable-diffusion",
|
|
106
|
+
"msg": "Stable Diffusion 1.5 is CreativeML OpenRAIL-M. The use restrictions in its attachment must be reproduced in every redistribution and derivative, and you must impose the same restrictions on your own users. A generic terms-of-service link does not satisfy the pass-through."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "sdxl-openrail-restrictions",
|
|
110
|
+
"sev": "warn",
|
|
111
|
+
"scope": "line",
|
|
112
|
+
"once": true,
|
|
113
|
+
"re": "sdxl|stable[-_ ]?diffusion[-_ ]?xl",
|
|
114
|
+
"msg": "SDXL is CreativeML OpenRAIL++-M. Same obligation as OpenRAIL-M: the behavioural use restrictions ride along with the weights, the fine-tunes and anything you serve from them, and they must be shown to your users - not just accepted by you."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "codestral-non-production",
|
|
118
|
+
"sev": "error",
|
|
119
|
+
"scope": "line",
|
|
120
|
+
"once": true,
|
|
121
|
+
"re": "codestral",
|
|
122
|
+
"msg": "Codestral weights are published under the Mistral AI Non-Production License: development, testing and research only. Serving Codestral to customers, or running it in an internal tool that supports the business, needs a commercial licence from Mistral. The free weights do not reach production, however small the deployment."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "mistral-research-weights",
|
|
126
|
+
"sev": "error",
|
|
127
|
+
"scope": "line",
|
|
128
|
+
"once": true,
|
|
129
|
+
"re": "mistral[-_ ]?large|pixtral[-_ ]?large",
|
|
130
|
+
"msg": "Mistral Large and Pixtral Large weights are released under a research or non-production licence, not Apache 2.0. Mistral 7B, Mixtral and Mistral NeMo are Apache 2.0 - if you need a local model from this family that survives a customer deployment, those are the identifiers to pin."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "qwen-100m-mau-cap",
|
|
134
|
+
"sev": "warn",
|
|
135
|
+
"scope": "line",
|
|
136
|
+
"once": true,
|
|
137
|
+
"re": "qwen[-_ ]?2\\.?5?[-_ ]?72b|qwen[-_ ]?max",
|
|
138
|
+
"msg": "The largest Qwen builds ship under the Qwen LICENSE rather than the Apache 2.0 terms the smaller sizes use: above 100 million monthly active users you must request a licence from the provider, and the attribution notice must survive redistribution. Check which parameter size you actually pinned before assuming Apache."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "non-commercial-artifact",
|
|
142
|
+
"sev": "error",
|
|
143
|
+
"scope": "line",
|
|
144
|
+
"once": true,
|
|
145
|
+
"re": "cc[-_ ]?by[-_ ]?nc|non[-_ ]?commercial|noncommercial",
|
|
146
|
+
"msg": "A non-commercial licence tag is referenced here. Non-commercial excludes any use primarily directed toward commercial advantage - which covers an internal tool at a for-profit company, not only a product you invoice for. Name the artefact in your notice file and obtain a commercial grant before launch."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "research-only-weights",
|
|
150
|
+
"sev": "error",
|
|
151
|
+
"scope": "line",
|
|
152
|
+
"once": true,
|
|
153
|
+
"re": "research[-_ ]only|non[-_ ]?production|research[-_ ]licen[cs]e|\\bMNPL\\b|\\bMRL\\b",
|
|
154
|
+
"msg": "Research-only or non-production weights are referenced. Those licences cover evaluation and internal research and stop there: the deployment becomes unlicensed on the day it answers its first real request, not on the day someone notices."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "gated-repo-acceptance",
|
|
158
|
+
"sev": "warn",
|
|
159
|
+
"scope": "line",
|
|
160
|
+
"once": true,
|
|
161
|
+
"re": "meta-llama/|google/gemma|stabilityai/stable-diffusion-3|mistralai/Mistral-Large",
|
|
162
|
+
"msg": "This is a gated model repository: the licence is accepted per account and the acceptance is logged against whoever clicked it. A personal account or a developer's token pulling these weights for the company does not put the acceptance on the company. Use an organisation account and keep the record with the release."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "unpinned-model-revision",
|
|
166
|
+
"sev": "warn",
|
|
167
|
+
"scope": "line",
|
|
168
|
+
"once": true,
|
|
169
|
+
"re": "from_pretrained\\s*\\(|snapshot_download\\s*\\(|huggingface\\.co/",
|
|
170
|
+
"not_line": "revision\\s*=|@[0-9a-f]{7,40}",
|
|
171
|
+
"needs_file": "meta-llama/|llama[-_ ]?[234][-_ .]|gemma|flux[.\\-_ ]?1|stable[-_ ]?diffusion|sdxl|codestral|mistral[-_ ]?large|pixtral",
|
|
172
|
+
"msg": "This model load has no revision pin. Licences change in place: weight repositories have shipped licence updates without a new model name, so the terms you reviewed are not necessarily the terms you ship. Pin a commit revision and the licence becomes reproducible evidence."
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "no-attribution-notice",
|
|
176
|
+
"sev": "warn",
|
|
177
|
+
"scope": "file",
|
|
178
|
+
"re": "meta-llama/|llama[-_ ]?[234][-_ .]|gemma|flux[.\\-_ ]?1|stable[-_ ]?diffusion|sdxl|codestral|mistral[-_ ]?large|pixtral",
|
|
179
|
+
"not_file": "NOTICE|THIRD[-_ ]?PARTY|ATTRIBUTION|model[-_ ]?card",
|
|
180
|
+
"msg": "Restricted-licence weights are referenced, but nothing here points at a NOTICE, THIRD_PARTY or model-card file carrying the licence text and attribution forward. Every licence flagged by this linter requires the text to travel with the code that uses it."
|
|
181
|
+
}
|
|
182
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "AI Model License Lint",
|
|
3
|
+
"subtitle": "Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture",
|
|
4
|
+
"bin": "ai-model-license-lint",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Scans the file you have open and names, for every open-weight model identifier, the exact licence clause that binds you and the case that breaks it - 22 rules, in VS Code and in the browser, no account. On the bundled 33-line fixture it returns 17 findings, 5 of them blocking, naming 7 licences; on the clean fixture it returns 0.",
|
|
7
|
+
"paid": "Exports the evidence: a generated NOTICE / attribution file and a dated per-model obligation report for the whole workspace, in the form procurement and counsel ask for, plus a CI exit code so a new model ID cannot land unreviewed.",
|
|
8
|
+
"need_key": "This option needs a licence (AI Model License Lint).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readystack-ai-model-license-lint
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture (bundled Node.js runtime)
|
|
5
|
+
Author-email: ReadyStack <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/ai-model-license-lint?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/ai-model-license-lint?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: llama license,model license,open weights,compliance,gemma terms,flux dev non-commercial,ai act,attribution
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: nodejs-wheel-binaries>=20
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# AI Model License Lint
|
|
22
|
+
|
|
23
|
+
> Runs the same checker as the npm package `@readystack/ai-model-license-lint` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
|
|
24
|
+
|
|
25
|
+
Reads the open-weight model IDs in your code and names the licence clause that binds you - 22 rules, 17 findings on a 33-line fixture
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
ai-model-license-lint file
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Node 18+. The same 22 rules as the VS Code extension, from a terminal or CI.
|
|
34
|
+
|
|
35
|
+
## Free
|
|
36
|
+
|
|
37
|
+
- Scans the file you have open and names, for every open-weight model identifier, the exact licence clause that binds you and the case that breaks it - 22 rules, in VS Code and in the browser, no account. On the bundled 33-line fixture it returns 17 findings, 5 of them blocking, naming 7 licences; on the clean fixture it returns 0.
|
|
38
|
+
- `--rules` lists every rule
|
|
39
|
+
|
|
40
|
+
## With a licence ($29 once)
|
|
41
|
+
|
|
42
|
+
- Exports the evidence: a generated NOTICE / attribution file and a dated per-model obligation report for the whole workspace, in the form procurement and counsel ask for, plus a CI exit code so a new model ID cannot land unreviewed.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
@readystack/ai-model-license-lint --dir ./templates --report html --out report.html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Outside technology counsel bills roughly USD 350-600 per hour for an open-source and model-licence review, repeated each time the model list changes
|
|
49
|
+
|
|
50
|
+
## Use from an AI agent (MCP)
|
|
51
|
+
|
|
52
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{ "mcpServers": { "ai-model-license-lint": { "command": "npx", "args": ["-y", "@readystack/ai-model-license-lint", "--mcp"] } } }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
59
|
+
|
|
60
|
+
## Use in CI
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
- name: AI Model License Lint
|
|
64
|
+
run: npx -y @readystack/ai-model-license-lint --dir . --ci
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/ai-model-license-lint --dir /work --ci`)
|
|
68
|
+
|
|
69
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
70
|
+
|
|
71
|
+
[Get a licence](https://buy.polar.sh/polar_cl_n48nKSGuBUvKyot5tlEi3ACu4uVv6EJrVAOvB2JgLDv)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
<!-- ai model license lint -->
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Install (PyPI)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
pip install readystack-ai-model-license-lint
|
|
81
|
+
ai-model-license-lint --help
|
|
82
|
+
```
|
readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/readystack_ai_model_license_lint/__init__.py
|
|
5
|
+
src/readystack_ai_model_license_lint/__main__.py
|
|
6
|
+
src/readystack_ai_model_license_lint.egg-info/PKG-INFO
|
|
7
|
+
src/readystack_ai_model_license_lint.egg-info/SOURCES.txt
|
|
8
|
+
src/readystack_ai_model_license_lint.egg-info/dependency_links.txt
|
|
9
|
+
src/readystack_ai_model_license_lint.egg-info/entry_points.txt
|
|
10
|
+
src/readystack_ai_model_license_lint.egg-info/requires.txt
|
|
11
|
+
src/readystack_ai_model_license_lint.egg-info/top_level.txt
|
|
12
|
+
src/readystack_ai_model_license_lint/js/cli.js
|
|
13
|
+
src/readystack_ai_model_license_lint/js/engine.js
|
|
14
|
+
src/readystack_ai_model_license_lint/js/license.js
|
|
15
|
+
src/readystack_ai_model_license_lint/js/package.json
|
|
16
|
+
src/readystack_ai_model_license_lint/js/rules.json
|
|
17
|
+
src/readystack_ai_model_license_lint/js/strings.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/requires.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs-wheel-binaries>=20
|
readystack_ai_model_license_lint-1.0.5/src/readystack_ai_model_license_lint.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
readystack_ai_model_license_lint
|