model-eol 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Sullivan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # model-eol
2
+
3
+ ![CI](https://github.com/thossullivan/model-eol/actions/workflows/ci.yml/badge.svg)
4
+ ![spec](https://img.shields.io/badge/spec-model--eol%2F0.1-blue)
5
+ ![deps](https://img.shields.io/badge/runtime%20deps-zero-brightgreen)
6
+ ![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
7
+ ![license](https://img.shields.io/badge/license-MIT-blue)
8
+
9
+ **A machine-readable deprecation feed format for AI models, plus the reference
10
+ tooling that turns it into a Dependabot for models.** v0.2.1.
11
+
12
+ ![model-eol demo: retired models, distributor clocks, worst-case date](docs/img/demo.gif)
13
+
14
+ ## The problem
15
+
16
+ On July 23, 2026, OpenAI shut down 18 models on schedule. Teams that pinned dated
17
+ snapshots - the provider's own recommended practice - got production 404s, because a
18
+ model ID is the one dependency your toolchain can't see. When an npm package is
19
+ deprecated, the warning prints at install, Dependabot opens the PR, the advisory
20
+ fails CI. A model's retirement date lives in an HTML docs page and an email.
21
+
22
+ Ordinary software solved this layer: endoflife.date for EOL data, OSV for
23
+ vulnerability feeds. This is the model version - a shared schema first, tooling
24
+ second, so the existing trackers can converge instead of each scraping alone.
25
+
26
+ ## What's here
27
+
28
+ - **`SPEC.md`** - the feed format (model-eol/0.1): id, aliases, announced, shutdown,
29
+ replacement, `distributions` for per-channel lifecycles, and publisher `policy`
30
+ floors. Small enough that a provider could serve it at
31
+ `/.well-known/model-eol.json` in an afternoon.
32
+ - **`feeds/`** - <!-- feeds-status -->Amazon (4 entries), Anthropic (29 entries), Google (64 entries) and OpenAI (194 entries), generated from the providers' live deprecation pages plus the AWS Bedrock lifecycle page, last verified 2026-08-03<!-- /feeds-status -->. Every
33
+ dated entry carries a source URL.
34
+ - **`check.mjs`** - zero-dependency CLI: CI gate, PR diff gate, inventory, CycloneDX
35
+ ML-BOM export, retirement schedule, alerts and badges, migration plan/apply.
36
+ - **`refresh/`** - regenerates the feeds from provider pages, models endpoints, and
37
+ distributor lifecycle pages, with a semantic diff for human review.
38
+ - **`bot/`** - the Dependabot part: a cron GitHub workflow that maintains one
39
+ migration PR or issue per retiring model.
40
+ - **`scripts/feed-changelog.mjs`** - the feeds' git history as Atom/markdown, so
41
+ "model retirements as they are announced" is a feed you can subscribe to.
42
+
43
+ ## Try it
44
+
45
+ Two commands, no install, no dependencies:
46
+
47
+ ```sh
48
+ git clone https://github.com/thossullivan/model-eol && cd model-eol
49
+ node check.mjs path/to/your/repo --days 90
50
+ ```
51
+
52
+ That is the whole CI gate. (`npx model-eol` lands with the npm publish.)
53
+
54
+ <details>
55
+ <summary><b>Every command</b> - PR gate, distributor clocks, SBOM, badges, migration plans</summary>
56
+
57
+ ```sh
58
+ # PR gate: flag only lines THIS change adds relative to the base ref
59
+ node check.mjs check . --changed origin/main --days 90
60
+ # the same repo judged by a distributor's clock instead of the publisher's
61
+ node check.mjs path/to/your/repo --via aws-bedrock
62
+ # inventory, and a CycloneDX 1.6 ML-BOM for your SBOM pipeline
63
+ node check.mjs inventory path/to/your/repo
64
+ node check.mjs inventory path/to/your/repo --format cyclonedx > model-bom.json
65
+ # retirement schedule with the repo-level worst case
66
+ node check.mjs schedule path/to/your/repo
67
+ # GitHub Actions annotations, Markdown, or a shields.io badge
68
+ node check.mjs alert path/to/your/repo --format github
69
+ node check.mjs alert path/to/your/repo --format badge > model-eol-badge.json
70
+ # migration plan (only high-confidence direct API refs are patchable) and safe apply
71
+ node check.mjs plan path/to/your/repo --days 90 > plan.json
72
+ node check.mjs apply --plan plan.json --dry-run
73
+ ```
74
+
75
+ </details>
76
+
77
+ Sample output against the test fixture (2026-08-01):
78
+
79
+ ```
80
+ ✗ app.py:2 o3-deep-research RETIRED 2026-07-23 (9 days ago) -> gpt-5.6-sol
81
+ ! app.py:3 claude-opus-4-1-20250805 RETIRES 2026-08-05 (4 days) -> claude-opus-4-8
82
+ · app.py:4 gpt-5.6-sol no retirement scheduled
83
+ earliest risk: 2026-07-23 (o3-deep-research-2025-06-26)
84
+ ```
85
+
86
+ Exit 1 on findings at or past the threshold - wire it into CI as-is. The badge JSON
87
+ plugs into the [shields endpoint](https://shields.io/endpoint) from any
88
+ CI-accessible URL.
89
+
90
+ **No API keys, no accounts.** Scanning is static analysis and the feeds are data
91
+ files, so the checker, PR gate, schedule, SBOM export, and badge all run with zero
92
+ credentials. The bot needs only a GitHub token (a fine-grained PAT if you want its
93
+ PRs to trigger checks), and provider API keys enter the picture in exactly two
94
+ optional places: your own eval hook command, and the feed-refresh models-endpoint
95
+ coverage.
96
+
97
+ ## Same weights, different clocks
98
+
99
+ The same model retires on different dates per channel, and a checker that ignores
100
+ this is wrong in both directions: o3-deep-research died at OpenAI on July 23 but
101
+ lives on Azure AI Foundry until December 26; claude-opus-4-1 retires at Anthropic on
102
+ August 5 but lives on Amazon Bedrock until January 8, 2027. `distributions` in the
103
+ spec carries these per-channel clocks, `--via <distributor>` judges your repo by the
104
+ channel you actually call, and the bedrock distributor fetcher keeps the clocks
105
+ current from AWS's own lifecycle page. Treat a distributor's later date as runway
106
+ for the same migration, not as a destination.
107
+
108
+ ## Policy floors - a forward guarantee from absence
109
+
110
+ A feed entry with no `announced` and no `shutdown` is an affirmative "nothing
111
+ scheduled as of `generated`" - something scraping HTML can never say. Publishers
112
+ with a stated minimum notice period (Anthropic commits to >=60 days) carry a
113
+ `policy` floor, so the schedule can say `guaranteed until <date> per anthropic
114
+ stated policy`. Stated policy, not a contract - the wording says so. OpenAI
115
+ publishes no formal floor, so its feed makes no forward claim.
116
+
117
+ ## Direct-first inventory
118
+
119
+ Direct API usage (OpenAI/Anthropic/Gemini/etc. SDK calls with real model IDs) is
120
+ scanned and checked immediately. Cloud providers and gateways - Azure Foundry,
121
+ Bedrock, Vertex, OpenRouter, LiteLLM, Portkey - often hide the model behind
122
+ deployment names or routing aliases, so those references stay in inventory as
123
+ resolver targets instead of pretending static code knows the deployed model:
124
+
125
+ ```
126
+ ? src/ai.ts:12 azure-ai-foundry Azure Foundry/OpenAI deployment reference
127
+ ? src/ai.ts:30 openrouter OpenRouter gateway reference
128
+ ```
129
+
130
+ Use `--scope direct` to fail CI only on what static analysis can prove. Model-like
131
+ strings absent from the feeds surface as non-failing **candidates**, so feed gaps
132
+ stay visible without generating false alarms.
133
+
134
+ The scanner covers code and configuration files with [`CODE_EXT`](lib/scanner.mjs)
135
+ as the source of truth - Python, TypeScript/JavaScript, JSON/YAML/TOML, shell,
136
+ Ruby, Go, Java, C#, Rust, C/C++, PHP, Swift, Kotlin, Terraform, SQL, Scala, and
137
+ Objective-C. Terraform files (`.tf`, `.tfvars`) are scanned while `.terraform`
138
+ directories stay excluded.
139
+
140
+ ## The bot
141
+
142
+ Copy `bot.yml.example` to `.github/workflows/model-eol-bot.yml` for a weekly run
143
+ that maintains exactly one labelled migration PR per retiring model (full feed
144
+ context in the body, never auto-merged) and one issue per finding that needs a
145
+ human. Dismissals are respected; a changed shutdown date reopens. Configure via
146
+ `.model-eol.json`: thresholds, scope, ignores, and an optional eval hook that runs
147
+ your own regression command against the replacement before the PR opens. Preview
148
+ everything with no GitHub calls:
149
+
150
+ ```sh
151
+ node bot/bot.mjs --dry-run --target-dir . --repo OWNER/REPO
152
+ ```
153
+
154
+ Two operational notes the hard way teaches: PRs created with `GITHUB_TOKEN` do not
155
+ trigger `pull_request` workflows (use a fine-grained PAT or GitHub App token when
156
+ checks must run), and the workflow splits privileges - provider keys live only in
157
+ the read-only plan/eval job, write tokens only in the publish job.
158
+
159
+ ## Keeping the feeds honest
160
+
161
+ ```sh
162
+ node refresh/refresh.mjs --check # semantic diff vs live pages; exit 3 = PR-worthy
163
+ node refresh/refresh.mjs --distributor aws-bedrock # distributor clocks from AWS's lifecycle page
164
+ node scripts/feed-changelog.mjs # feeds' git history as an Atom feed
165
+ ```
166
+
167
+ Parse failures fail loudly and never emit a guessed feed. This runs automatically:
168
+ a weekly workflow (`.github/workflows/feed-refresh.yml`, Mondays 05:23 UTC) checks
169
+ the live sources and opens a PR with the semantic diff - and an auto-updated
170
+ README freshness line - when anything material changed. The first live runs earned
171
+ their keep: they caught the hand-compiled feeds drifting from Anthropic's
172
+ recommendations within one week, and corrected a hand-compiled Bedrock date that
173
+ was three months wrong.
174
+
175
+ ## The actual ask
176
+
177
+ The endgame is not "another checker"; it's the format. Providers already publish
178
+ deprecation *pages* because customers asked. A `retirement_date` field in the models
179
+ endpoint, or this feed at a well-known URL, turns every existing tracker into an
180
+ ecosystem. endoflife.date now tracks Claude with nearly this field set - the
181
+ convergence is already starting. If you maintain one of the community trackers:
182
+ converging on a shared schema - this one or a better one - is the highest-leverage
183
+ move any of us can make here.
184
+
185
+ ## Status / honesty
186
+
187
+ - Feeds refresh automatically: the weekly feed-refresh workflow is live in this
188
+ repo (parse failures fail the run; material changes become a reviewed PR), and
189
+ CI runs the full suite on every push and PR. The bot workflow still ships as
190
+ `bot.yml.example`.
191
+ - Current-model entries (and therefore policy-floor horizons) populate only when
192
+ refresh runs with provider API keys for the models endpoints.
193
+ - The checker matches known IDs only - it will not discover models absent from the
194
+ feeds. Deliberate: precision over discovery for a CI gate.
195
+ - Fetchers: OpenAI, Anthropic, Google, aws-bedrock, and vertex-ai are live. Azure
196
+ clocks are carried where OpenAI's own page publishes them; a standalone Azure
197
+ lifecycle fetcher is not built yet.
198
+ - Not yet: feed signing, gateway route resolvers, npm publish (prepped, pending).
199
+
200
+ ## Contributing
201
+
202
+ The most valuable contribution is a feed correction with a source URL - and the
203
+ one rule is that `feeds/*.json` are generated, never hand-edited. See
204
+ [CONTRIBUTING.md](CONTRIBUTING.md) for that workflow, the zero-dependency
205
+ doctrine, and what a new publisher parser needs.
package/SPEC.md ADDED
@@ -0,0 +1,134 @@
1
+ # model-eol - a machine-readable model deprecation feed (draft spec v0.1)
2
+
3
+ > Status: sketch, 2026-07-25. The problem: model retirement dates live in HTML docs
4
+ > and emails, so every deprecation checker is a scraper with a private registry
5
+ > format. Ordinary software solved this layer - endoflife.date for EOL data, OSV for
6
+ > vulnerability feeds. This is the model version: small enough that a provider could
7
+ > ship it in an afternoon, useful enough that scrapers converge on it in the meantime.
8
+
9
+ ## The format
10
+
11
+ A feed is a JSON document: metadata + a list of model entries.
12
+
13
+ ```json
14
+ {
15
+ "spec": "model-eol/0.1",
16
+ "publisher": "openai",
17
+ "generated": "2026-07-25T00:00:00Z",
18
+ "source": "https://developers.openai.com/api/docs/deprecations",
19
+ "models": [
20
+ {
21
+ "id": "o3-deep-research-2025-06-26",
22
+ "aliases": ["o3-deep-research"],
23
+ "announced": "2026-04-22",
24
+ "shutdown": "2026-07-23",
25
+ "replacement": "gpt-5.6-sol",
26
+ "notes": "Deep-research post-trained agent; replacement is a general model + tools, not a structural successor.",
27
+ "distributions": [
28
+ { "via": "azure-ai-foundry", "shutdown": "2026-12-26",
29
+ "source": "https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/model-retirements" }
30
+ ]
31
+ },
32
+ {
33
+ "id": "gpt-5.6-sol"
34
+ }
35
+ ]
36
+ }
37
+ ```
38
+
39
+ ### Fields
40
+
41
+ | Field | Req | Meaning |
42
+ |---|---|---|
43
+ | `id` | yes | The exact model identifier as sent over the wire (dated snapshot preferred) |
44
+ | `aliases` | no | Other strings that resolve to this model (undated aliases, marketing names) |
45
+ | `announced` | no | Date the deprecation was announced (absent = not deprecated) |
46
+ | `shutdown` | no | Date calls stop working on the publisher's own API (absent = none scheduled) |
47
+ | `replacement` | no | A single exact model ID that resolves against an id or alias in this feed and is safe as a drop-in string substitution. This is the only field plan/apply may patch from |
48
+ | `replacement_options` | no | An ordered array of exact model IDs for multiple or unresolved choices. These are issue-only and need not resolve in this feed |
49
+ | `replacement_note` | no | Free-text guidance such as parameter requirements or provider platform alternatives |
50
+ | `notes` | no | Human context: structural differences, migration gotchas |
51
+ | `distributions` | no | Per-distributor lifecycles - the same weights on different clocks (Azure, Bedrock, Vertex). Each entry: `via`, optional `announced`/`shutdown`/`status`/`date_precision`, `source` |
52
+ | `date_precision` | no | Qualifies `shutdown`: absent or `"exact"` means the stated day; `"earliest"` means the provider commits only that the event happens no sooner (Google's "earliest possible" dates). Consumers treat an earliest date as the scheduled date - it is the soonest legal death - and display it as a lower bound |
53
+
54
+ Dates are ISO 8601, UTC. A model with no `announced` and no `shutdown` is an
55
+ affirmative statement of "no retirement scheduled as of `generated`" - the absence
56
+ is data, which is exactly what scraping HTML can never give you.
57
+
58
+ `distributions[].status`, when present, is one of `active`, `legacy`,
59
+ `extended-access`, or `retired`. `legacy` means no new consumers but existing
60
+ usage works; `extended-access` means availability continues past the stated
61
+ `shutdown` under a paid or opt-in program (Bedrock offers this for some models) -
62
+ the `shutdown` date stays the base retirement, and the status flags that a
63
+ negotiated tail exists.
64
+
65
+ ### Structured replacement contract
66
+
67
+ `replacement` is a single exact model ID that resolves against an id or alias in
68
+ the same feed and is safe as a drop-in string substitution. This is the ONLY
69
+ field plan/apply may patch from. Otherwise absent.
70
+
71
+ `replacement_options` is an optional array of exact model IDs. Each option must
72
+ pass the identifier grammar, but feed resolution is NOT required since a target
73
+ may not be carried yet. Options are ordered as the provider lists them.
74
+
75
+ `replacement_note` is optional free-text guidance. It can carry parameter
76
+ requirements such as `reasoning.mode: pro` or prose alternatives such as Google's
77
+ platform pointers.
78
+
79
+ Replacement options and notes are issue-only. The planner never auto-patches
80
+ from options; it patches `replacement` exclusively.
81
+
82
+ ### Policy floors
83
+
84
+ A feed MAY include a publisher's stated minimum deprecation notice period:
85
+
86
+ ```json
87
+ "policy": {
88
+ "min_notice_days": 60,
89
+ "source": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
90
+ }
91
+ ```
92
+
93
+ For an entry with neither `announced` nor `shutdown`, a policy floor means the
94
+ entry cannot become unavailable before `generated` + `min_notice_days` days,
95
+ provided the provider honors its stated policy. This is a stated policy, not a
96
+ contract or a guarantee that the provider will remain available.
97
+
98
+ The `policy` field is optional. Its absence means no forward claim can be made,
99
+ intentionally. OpenAI publishes no formal notice floor, so the OpenAI feed carries
100
+ no `policy` field.
101
+
102
+ ### Why `distributions` is in the core and not an extension
103
+
104
+ The July 2026 evidence: OpenAI shut off `o3-deep-research` on July 23; Azure lists it
105
+ retiring December 26. Anthropic retired Claude 3.7 Sonnet on February 19; Bedrock
106
+ kept it to April 28. The same BOM entry has different sourcing terms per channel, and
107
+ a checker that doesn't know that will tell an Azure shop their model is dead five
108
+ months early - or tell an OpenAI-direct shop they're fine because Azure says so.
109
+
110
+ ## Serving it
111
+
112
+ - **Providers** (the ask): serve the feed at `/.well-known/model-eol.json`, or add
113
+ `announced`/`shutdown`/`replacement` to the existing models endpoint. Either kills
114
+ the scraping industry overnight.
115
+ - **Aggregators** (the interim): community datasets re-publish provider pages in this
116
+ format, with `source` on every entry so claims are auditable. Multiple feeds
117
+ compose - a checker loads provider feeds where they exist and community feeds where
118
+ they don't.
119
+ - **Consumers**: anything that can read JSON - a CI step, a dashboard, a Dependabot
120
+ clone that opens migration PRs when `shutdown - today < threshold`.
121
+
122
+ ## Non-goals (v0.1)
123
+
124
+ Pricing, capabilities, context windows (models.dev and friends already do this);
125
+ behavior-change tracking (that's an eval suite's job, not a manifest's); signing and
126
+ provenance (worth doing the moment anyone depends on this; premature today).
127
+
128
+ ## Prior art, gratefully
129
+
130
+ endoflife.date (the shape of the solution), OSV (proof that a shared schema turns N
131
+ scrapers into an ecosystem), CycloneDX ML-BOM / SPDX AI profile (where an `eol` field
132
+ belongs once feeds exist), and the current scrapers - llm-model-deprecation,
133
+ llmstatus.ai, llm-info, ai-model-watch-data, modelradar-data - which are the demand
134
+ signal this spec is trying to give a common target.
package/action.yml ADDED
@@ -0,0 +1,71 @@
1
+ name: 'model-eol check'
2
+ description: 'Fail CI when a pinned AI model ID is retired or retiring within a threshold, using model-eol feeds.'
3
+ branding:
4
+ icon: 'alert-triangle'
5
+ color: 'red'
6
+ inputs:
7
+ command:
8
+ description: 'Command to run: check, inventory, or schedule'
9
+ required: false
10
+ default: 'check'
11
+ paths:
12
+ description: 'Paths to scan. Newline-separated paths are recommended for paths containing spaces; single-line whitespace-separated paths remain supported for legacy compatibility.'
13
+ required: false
14
+ default: '.'
15
+ days:
16
+ description: 'Fail when a model retires within this many days'
17
+ required: false
18
+ default: '90'
19
+ via:
20
+ description: 'Judge lifecycles by a distributor clock (e.g. azure-ai-foundry, aws-bedrock) instead of the publisher''s'
21
+ required: false
22
+ default: ''
23
+ scope:
24
+ description: 'Check scope: all tracked model IDs, or direct to leave cloud/gateway refs as inventory hints'
25
+ required: false
26
+ default: 'all'
27
+ changed:
28
+ description: 'Base ref for the PR gate: check only lines added relative to this ref (requires fetch-depth: 0)'
29
+ required: false
30
+ default: ''
31
+ format:
32
+ description: 'Alert output format when command is alert: github, markdown, or badge'
33
+ required: false
34
+ default: 'github'
35
+ feeds:
36
+ description: 'Directory of model-eol feeds (defaults to the feeds bundled with the action)'
37
+ required: false
38
+ default: ''
39
+ runs:
40
+ using: 'composite'
41
+ steps:
42
+ - name: model-eol check
43
+ shell: bash
44
+ env:
45
+ MODEL_EOL_COMMAND: ${{ inputs.command }}
46
+ MODEL_EOL_PATHS: ${{ inputs.paths }}
47
+ MODEL_EOL_DAYS: ${{ inputs.days }}
48
+ MODEL_EOL_VIA: ${{ inputs.via }}
49
+ MODEL_EOL_CHANGED: ${{ inputs.changed }}
50
+ MODEL_EOL_SCOPE: ${{ inputs.scope }}
51
+ MODEL_EOL_FORMAT: ${{ inputs.format }}
52
+ MODEL_EOL_FEEDS: ${{ inputs.feeds }}
53
+ run: |
54
+ split_model_eol_paths() {
55
+ PATH_ARGS=()
56
+ if [[ "$MODEL_EOL_PATHS" == *$'\n'* ]]; then
57
+ while IFS= read -r path || [[ -n "$path" ]]; do
58
+ if [[ -n "$path" ]]; then PATH_ARGS+=("$path"); fi
59
+ done <<< "$MODEL_EOL_PATHS"
60
+ else
61
+ read -r -a PATH_ARGS <<< "$MODEL_EOL_PATHS"
62
+ fi
63
+ }
64
+ split_model_eol_paths
65
+ ARGS=("$MODEL_EOL_COMMAND" "${PATH_ARGS[@]}" --days "$MODEL_EOL_DAYS")
66
+ if [ -n "$MODEL_EOL_VIA" ]; then ARGS+=(--via "$MODEL_EOL_VIA"); fi
67
+ if [ -n "$MODEL_EOL_CHANGED" ]; then ARGS+=(--changed "$MODEL_EOL_CHANGED"); fi
68
+ if [ -n "$MODEL_EOL_SCOPE" ]; then ARGS+=(--scope "$MODEL_EOL_SCOPE"); fi
69
+ if [ -n "$MODEL_EOL_FORMAT" ]; then ARGS+=(--format "$MODEL_EOL_FORMAT"); fi
70
+ if [ -n "$MODEL_EOL_FEEDS" ]; then ARGS+=(--feeds "$MODEL_EOL_FEEDS"); fi
71
+ node "${{ github.action_path }}/check.mjs" "${ARGS[@]}"