@witchpot/gmc 0.8.0 → 0.10.0
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/CHANGELOG.md +52 -0
- package/package.json +1 -2
- package/skill/gmc-analysis/CHANGELOG.md +25 -0
- package/skill/gmc-analysis/SKILL.md +42 -3
- package/skill/gmc-analysis.manifest.json +2 -2
- package/src/args.js +9 -1
- package/src/constants.js +13 -2
- package/src/help.js +99 -0
- package/src/main.js +7 -1
- package/src/main.test.js +130 -0
- package/src/projection.js +21 -0
- package/src/requests.js +234 -0
- package/src/requests.test.js +369 -3
- package/scripts/check-release-readiness.mjs +0 -177
- package/scripts/gen-skill-manifest.mjs +0 -151
- package/scripts/smoke-beta.mjs +0 -246
- package/scripts/smoke-pack.mjs +0 -123
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.10.0 - 2026-09-22
|
|
6
|
+
|
|
7
|
+
- New `gmc roblox` command family (NAK-715, Provisional): `charts` (one
|
|
8
|
+
recorded ranking cohort as of a cutoff), `experiences` (the tracked
|
|
9
|
+
catalog with filters, sorts and signed-cursor pagination), `experience
|
|
10
|
+
<universeId>` (one experience with per-metric observation times),
|
|
11
|
+
`history <universeId> --granularity hourly|daily` (retained observations
|
|
12
|
+
with explicit gaps), and `coverage` (the configured cohort matrix with
|
|
13
|
+
per-dimension freshness). Universe ids are decimal strings end to end —
|
|
14
|
+
they are never Steam appids, and the commands reject one. The Roblox
|
|
15
|
+
surface went live in production on 2026-09-22 (NAK-725); the commands
|
|
16
|
+
stay Provisional until the surface has been through a broader real-data
|
|
17
|
+
smoke period.
|
|
18
|
+
|
|
19
|
+
- The Roblox read endpoints reject impossible calendar dates in every
|
|
20
|
+
offset spelling (`as_of=2026-02-31T12:00:00+09:00` is now a 400 naming
|
|
21
|
+
the nonexistent date instead of silently answering for March 3), and
|
|
22
|
+
history gap counts always reconcile with the expected point count.
|
|
23
|
+
|
|
24
|
+
- Not yet mapped: the saved-experiences surface (`/api/v1/roblox/saved`,
|
|
25
|
+
NAK-723) is live over REST and MCP; a CLI mapping will follow.
|
|
26
|
+
|
|
27
|
+
## 0.9.0 - 2026-09-16
|
|
28
|
+
|
|
29
|
+
- New `--release-status <released|upcoming|tba>` filter on the games
|
|
30
|
+
search/count/aggregate commands (NAK-803), forwarded verbatim and
|
|
31
|
+
case-sensitively as the `releaseStatus` query param. The parameter was
|
|
32
|
+
already in the REST contract; the CLI simply had no mapping for it, which
|
|
33
|
+
forced CLI recipes for "released games only" to lean on the release window
|
|
34
|
+
alone. The filter is an exact match layered on top of the guard that
|
|
35
|
+
already drops any title still marked `upcoming` past its provisional
|
|
36
|
+
release date.
|
|
37
|
+
|
|
38
|
+
- New command `gmc games ai-disclosure <appid> --json` (NAK-804): one title's
|
|
39
|
+
developer-authored AI-use disclosure text, the store page it was read from,
|
|
40
|
+
and its observed history of change. It takes the appid alone — no `--source`
|
|
41
|
+
selector and no query parameters, matching the route — and rejects any other
|
|
42
|
+
flag as a usage error rather than ignoring it. `absent` means no disclosure
|
|
43
|
+
block was observed on the store page, never that the title uses no
|
|
44
|
+
generative AI; `unconfirmed` includes titles never checked, and is also what
|
|
45
|
+
an appid never observed returns, since the route has no existence check.
|
|
46
|
+
Listed as Provisional in the beta command set.
|
|
47
|
+
- Bundled `gmc-analysis` skill bumped to 0.16.0 with a "Disclosure text and
|
|
48
|
+
change history" subsection covering that command and how to report its text
|
|
49
|
+
and events honestly.
|
|
50
|
+
- An inapplicable `--preset` is now rejected as a usage error BEFORE the
|
|
51
|
+
metered request, for every command. Previously the request executed (and
|
|
52
|
+
charged a credit) and the preset was only rejected while shaping the output.
|
|
53
|
+
- Packaging: `scripts/` (release tooling) no longer ships in the npm tarball,
|
|
54
|
+
and the release-readiness gate now scans every file the tarball actually
|
|
55
|
+
contains instead of a hardcoded source list.
|
|
56
|
+
|
|
5
57
|
## 0.8.0 - 2026-09-14
|
|
6
58
|
|
|
7
59
|
For npm users this release lands together with 0.7.0's content, because
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchpot/gmc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tools for Game Market Copilot market research.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"bin",
|
|
35
35
|
"src",
|
|
36
|
-
"scripts",
|
|
37
36
|
"skill",
|
|
38
37
|
"README.md",
|
|
39
38
|
"CHANGELOG.md"
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# gmc-analysis skill changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-09-16
|
|
4
|
+
|
|
5
|
+
- New "Disclosure text and change history" subsection under "AI disclosure
|
|
6
|
+
state" (NAK-804): the surface behind the tri-state flag — the developer's
|
|
7
|
+
disclosure text, the store page it was read from, the first and latest
|
|
8
|
+
observation dates, and the observed change history. Reached with
|
|
9
|
+
`gmc games ai-disclosure <appid>` (appid only, no `--source` and no query
|
|
10
|
+
parameters) or `game_profile` with `ai_disclosure` in `sections` (opt-in, so
|
|
11
|
+
a call that does not name it performs no extra read, and raw-read weight
|
|
12
|
+
class, so asking for it beside `detail` leaves the per-appid cost at 1).
|
|
13
|
+
- Adds the interpretation guardrails as honesty rules: `disclosureText` is the
|
|
14
|
+
developer's own wording, normalized only for whitespace and never classified
|
|
15
|
+
or scored by Game Market Copilot, so it is quoted and attributed to them
|
|
16
|
+
rather than paraphrased into a verdict; each event is an OBSERVED change
|
|
17
|
+
dated as the window between `previousObservedAt` and `observedAt`, never as
|
|
18
|
+
a moment; an empty `events` array means no change was observed, never that
|
|
19
|
+
nothing changed, because a title's first observation is a baseline that
|
|
20
|
+
never appears as an event; `events` can be non-empty while `state` is
|
|
21
|
+
`unconfirmed`; `unconfirmed` still covers titles never checked, since this
|
|
22
|
+
surface has no existence check; a `null` payload carries
|
|
23
|
+
`ai_disclosure_detail_unavailable` and means the read could not be reached,
|
|
24
|
+
never "not disclosed".
|
|
25
|
+
- MCP-only mapping updated: per-title AI-disclosure text and change history ->
|
|
26
|
+
`game_profile` with `ai_disclosure` in `sections`.
|
|
27
|
+
|
|
3
28
|
## 0.15.0 - 2026-09-14
|
|
4
29
|
|
|
5
30
|
- New "AI disclosure state" section in SKILL.md (NAK-773): the tri-state
|
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
or campaigns that GMC can answer — including turning results into
|
|
9
9
|
publish-ready branded chart cards.
|
|
10
10
|
metadata:
|
|
11
|
-
version: 0.
|
|
11
|
+
version: 0.16.0
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# GMC Analysis
|
|
@@ -57,7 +57,9 @@ applies identically. Mechanics map as follows:
|
|
|
57
57
|
-> `more_like_this`; review topics by review language (`games
|
|
58
58
|
language-topics`) -> `game_language_topics`; review-count history with
|
|
59
59
|
its lifecycle/announcement/discount context (`games review-history`) ->
|
|
60
|
-
`game_review_history
|
|
60
|
+
`game_review_history`; per-title AI-disclosure text and change history
|
|
61
|
+
(`games ai-disclosure`) -> `game_profile` with `ai_disclosure` in
|
|
62
|
+
`sections`.
|
|
61
63
|
- Per-family collection status ("has GMC actually collected X for this
|
|
62
64
|
game?") has no CLI command yet and is MCP-only: `game_collection_status`
|
|
63
65
|
(see the dedicated section below).
|
|
@@ -483,7 +485,8 @@ Every title carries a tri-state AI-disclosure read from its Steam store page:
|
|
|
483
485
|
search/count and `market_aggregate`; group with
|
|
484
486
|
`--group-by ai_disclosure` (it cross-tabs with `release_month`); read one
|
|
485
487
|
title's state from `detail.aiDisclosure` (`state`, `observedAt`,
|
|
486
|
-
`rawCategoryValue`).
|
|
488
|
+
`rawCategoryValue`). The text and history behind that flag are a separate
|
|
489
|
+
surface — see the subsection below.
|
|
487
490
|
|
|
488
491
|
Interpretation guardrails (honesty rules, not suggestions):
|
|
489
492
|
|
|
@@ -505,6 +508,42 @@ Interpretation guardrails (honesty rules, not suggestions):
|
|
|
505
508
|
(the underlying rollups have no disclosure predicate). Size disclosure
|
|
506
509
|
cohorts with `market_aggregate` instead.
|
|
507
510
|
|
|
511
|
+
### Disclosure text and change history
|
|
512
|
+
|
|
513
|
+
`gmc games ai-disclosure <appid> --json` (MCP: `game_profile` with
|
|
514
|
+
`ai_disclosure` in `sections`) returns what sits behind that flag: the
|
|
515
|
+
developer's disclosure text, the store page it was read from, the first and
|
|
516
|
+
latest observation dates, and the observed change history. The CLI command
|
|
517
|
+
takes the appid and nothing else — no `--source`, no query parameters. Over
|
|
518
|
+
MCP the section is opt-in, so a call that does not name it performs no extra
|
|
519
|
+
read, and it stays in the raw-read weight class: asking for it beside `detail`
|
|
520
|
+
leaves the per-appid cost at 1.
|
|
521
|
+
|
|
522
|
+
Interpretation guardrails (honesty rules, not suggestions):
|
|
523
|
+
|
|
524
|
+
- **The text is the developer's, never ours.** `disclosureText` is their own
|
|
525
|
+
wording as read from the store page, normalized only for whitespace. Game
|
|
526
|
+
Market Copilot never classifies it, scores it, or infers from it how much AI
|
|
527
|
+
was used. Quote it and attribute it to the developer; never paraphrase it
|
|
528
|
+
into a verdict of your own. It is `null` whenever `state` is not
|
|
529
|
+
`disclosed`.
|
|
530
|
+
- **Each event is an observed change, so date it as a window.** The store page
|
|
531
|
+
is read periodically, not continuously, so a change happened SOMEWHERE
|
|
532
|
+
between `previousObservedAt` and `observedAt`. Say "between those two
|
|
533
|
+
reads"; never say it happened on either date.
|
|
534
|
+
- **An empty `events` array is not "nothing ever changed".** A title's FIRST
|
|
535
|
+
observation is a baseline and never appears as an event, so an empty array
|
|
536
|
+
means only that no change has been observed between two reads. `events` can
|
|
537
|
+
also be non-empty while `state` is `unconfirmed` — history outlives a later
|
|
538
|
+
read that failed.
|
|
539
|
+
- **`unconfirmed` still includes never checked.** This surface has no
|
|
540
|
+
existence check, so an appid never observed comes back `unconfirmed` with no
|
|
541
|
+
events rather than an error. That is a statement about what has been
|
|
542
|
+
observed, never about the title.
|
|
543
|
+
- **A `null` payload is unavailability.** It carries
|
|
544
|
+
`ai_disclosure_detail_unavailable` and means the read could not be reached.
|
|
545
|
+
Never report it as "not disclosed", and never as not collected.
|
|
546
|
+
|
|
508
547
|
## Hard rules
|
|
509
548
|
|
|
510
549
|
- **Missing data is not absent sentiment.** Report `cohort.usableGames`,
|
package/src/args.js
CHANGED
|
@@ -90,6 +90,11 @@ function isCommandToken(command, token) {
|
|
|
90
90
|
// unknown command `steam` with a stray positional rather than reaching
|
|
91
91
|
// the tag-trends branch.
|
|
92
92
|
"steam",
|
|
93
|
+
// NAK-715. Same trap as `steam` above: a token missing from this list
|
|
94
|
+
// falls through to positional rather than erroring, so `gmc roblox
|
|
95
|
+
// charts` would parse as the unknown command `roblox` with a stray
|
|
96
|
+
// positional instead of reaching the charts branch.
|
|
97
|
+
"roblox",
|
|
93
98
|
"auth",
|
|
94
99
|
"login",
|
|
95
100
|
"logout",
|
|
@@ -109,7 +114,7 @@ function isCommandToken(command, token) {
|
|
|
109
114
|
return ["install", "print", "status"].includes(token);
|
|
110
115
|
}
|
|
111
116
|
if (command[0] === "games") {
|
|
112
|
-
return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this"].includes(token);
|
|
117
|
+
return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this", "ai-disclosure"].includes(token);
|
|
113
118
|
}
|
|
114
119
|
if (command[0] === "reviews") {
|
|
115
120
|
return token === "patterns";
|
|
@@ -138,6 +143,9 @@ function isCommandToken(command, token) {
|
|
|
138
143
|
if (command[0] === "steam") {
|
|
139
144
|
return ["tag-trends", "trend-stories"].includes(token);
|
|
140
145
|
}
|
|
146
|
+
if (command[0] === "roblox") {
|
|
147
|
+
return ["charts", "experiences", "experience", "history", "coverage"].includes(token);
|
|
148
|
+
}
|
|
141
149
|
return false;
|
|
142
150
|
}
|
|
143
151
|
|
package/src/constants.js
CHANGED
|
@@ -7,7 +7,7 @@ export const EXIT_CONFIG = 3;
|
|
|
7
7
|
export const EXIT_API = 4;
|
|
8
8
|
|
|
9
9
|
export const CLI_PACKAGE_NAME = "@witchpot/gmc";
|
|
10
|
-
export const CLI_VERSION = "0.
|
|
10
|
+
export const CLI_VERSION = "0.10.0";
|
|
11
11
|
|
|
12
12
|
export const DEFAULT_API_BASE_URL = "https://gamemarketcopilot.com";
|
|
13
13
|
export const DEFAULT_CONFIG_PATH = path.join(homedir(), ".config", "gmc", "config.json");
|
|
@@ -60,7 +60,18 @@ export const PROVISIONAL_BETA_COMMANDS = [
|
|
|
60
60
|
// default-off, so claiming supported-beta status would promise a stability
|
|
61
61
|
// the feature flag has not yet been asked to keep.
|
|
62
62
|
"gmc steam trend-stories --locale en --json",
|
|
63
|
-
"gmc steam tag-trends --view featured|board|group|table|tag --json"
|
|
63
|
+
"gmc steam tag-trends --view featured|board|group|table|tag --json",
|
|
64
|
+
// NAK-804. New command; starts Provisional per the closed-beta roadmap's
|
|
65
|
+
// default for anything not yet through broader real-data smoke testing.
|
|
66
|
+
"gmc games ai-disclosure <appid> --json",
|
|
67
|
+
// NAK-715/NAK-725. Enabled in production on 2026-09-22; stays Provisional
|
|
68
|
+
// per the closed-beta roadmap's default until the surface has been through
|
|
69
|
+
// a broader real-data smoke period (the NAK-804 rule), then graduates.
|
|
70
|
+
"gmc roblox charts --country all --sort top-playing-now --json",
|
|
71
|
+
"gmc roblox experiences --country all --limit 50 --json",
|
|
72
|
+
"gmc roblox experience <universeId> --json",
|
|
73
|
+
"gmc roblox history <universeId> --granularity daily|hourly --json",
|
|
74
|
+
"gmc roblox coverage --json"
|
|
64
75
|
];
|
|
65
76
|
|
|
66
77
|
export const COMPARE_FOCUSES = ["overview", "market", "reviews", "marketing", "audience", "showcases"];
|
package/src/help.js
CHANGED
|
@@ -51,6 +51,7 @@ Usage:
|
|
|
51
51
|
gmc games language-topics --source steam <externalId> --min-reviews <number> --include-insufficient --json
|
|
52
52
|
gmc games review-history --source steam <externalId> --json
|
|
53
53
|
gmc games more-like-this --source steam <externalId> --country <country> --json
|
|
54
|
+
gmc games ai-disclosure <appid> --json
|
|
54
55
|
gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
|
|
55
56
|
gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --sample-size <number> --preset summary --json
|
|
56
57
|
gmc marketing patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
|
|
@@ -69,6 +70,11 @@ Usage:
|
|
|
69
70
|
gmc steam tag-trends --view featured --json
|
|
70
71
|
gmc steam tag-trends --view table --sort release_share_delta --order desc --json
|
|
71
72
|
gmc steam tag-trends --view tag --tag <tag_slug> --json
|
|
73
|
+
gmc roblox charts --country all --sort top-playing-now --json
|
|
74
|
+
gmc roblox experiences --country all --limit 50 --json
|
|
75
|
+
gmc roblox experience <universeId> --json
|
|
76
|
+
gmc roblox history <universeId> --granularity daily --json
|
|
77
|
+
gmc roblox coverage --json
|
|
72
78
|
|
|
73
79
|
Tag taxonomy:
|
|
74
80
|
gmc tags --json List the full tag dictionary (slug, name).
|
|
@@ -151,6 +157,31 @@ Review-count history:
|
|
|
151
157
|
Examples:
|
|
152
158
|
gmc games review-history --source steam 730 --json
|
|
153
159
|
|
|
160
|
+
AI-disclosure text and history:
|
|
161
|
+
gmc games ai-disclosure <appid> --json
|
|
162
|
+
One game's developer-authored AI-use
|
|
163
|
+
disclosure text, the store page it was
|
|
164
|
+
read from, and its observed history of
|
|
165
|
+
change. There are no parameters beyond
|
|
166
|
+
the appid.
|
|
167
|
+
|
|
168
|
+
data.state is disclosed, absent, or unconfirmed. absent means no
|
|
169
|
+
disclosure block was observed on the store page -- never that the title
|
|
170
|
+
uses no generative AI. unconfirmed means Game Market Copilot has not yet
|
|
171
|
+
successfully observed the store page either way, including a title never
|
|
172
|
+
attempted at all.
|
|
173
|
+
|
|
174
|
+
data.disclosureText is the developer's own wording, normalized only for
|
|
175
|
+
whitespace and never classified or summarized by Game Market Copilot. It
|
|
176
|
+
is null unless state is disclosed. data.sourceUrl is the store page the
|
|
177
|
+
observation was read from.
|
|
178
|
+
|
|
179
|
+
data.events lists only OBSERVED changes to the state, oldest first; a
|
|
180
|
+
title's first observation is a baseline and never appears as an event.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
gmc games ai-disclosure 730 --json
|
|
184
|
+
|
|
154
185
|
Steam tag trends:
|
|
155
186
|
gmc steam tag-trends --view <view> --json
|
|
156
187
|
Tag supply trends over one frozen monthly
|
|
@@ -206,6 +237,70 @@ Steam tag trends:
|
|
|
206
237
|
gmc steam tag-trends --view table --sort current_share --limit 50 --json
|
|
207
238
|
gmc steam tag-trends --view tag --tag cozy --json
|
|
208
239
|
|
|
240
|
+
Roblox:
|
|
241
|
+
gmc roblox charts --json One ranking cohort's stored chart
|
|
242
|
+
snapshot as of a cutoff.
|
|
243
|
+
gmc roblox experiences --json Browse the tracked cohort with coverage.
|
|
244
|
+
gmc roblox experience <universeId> --json
|
|
245
|
+
One experience: metadata, metrics,
|
|
246
|
+
artwork.
|
|
247
|
+
gmc roblox history <universeId> --json
|
|
248
|
+
A bounded window of stored metric
|
|
249
|
+
observations.
|
|
250
|
+
gmc roblox coverage --json What the surface has, per dimension.
|
|
251
|
+
--country <country> all | US | JP. Selects which CHART was
|
|
252
|
+
read, never a regional metric scope.
|
|
253
|
+
--device <device> computer.
|
|
254
|
+
--sort <sort> top-playing-now | top-trending |
|
|
255
|
+
top-revisited | fun-with-friends.
|
|
256
|
+
--as-of <instant> ISO 8601 cutoff; defaults to now. No
|
|
257
|
+
observation after it is considered.
|
|
258
|
+
--eligibility-days <number> experiences only, 1-30 (default 7). How
|
|
259
|
+
far back a chart appearance still counts.
|
|
260
|
+
--min-player-count <number> experiences only, on the resolved row.
|
|
261
|
+
--max-rank <number> experiences only, on the resolved row.
|
|
262
|
+
--genre <genre> experiences only, exact match.
|
|
263
|
+
--creator-type <type> experiences only, e.g. Group or User.
|
|
264
|
+
--detail <enriched|chart_only> experiences only. Omitted returns both.
|
|
265
|
+
--order-by <key> rank | player_count | last_chart_seen |
|
|
266
|
+
universe_id (experiences only).
|
|
267
|
+
--order <asc|desc> experiences only.
|
|
268
|
+
--granularity <hourly|daily> history only. Defaults to daily.
|
|
269
|
+
--from <instant> --to <instant> history only. Hourly windows span at most
|
|
270
|
+
90 days, daily at most 365.
|
|
271
|
+
--limit <number> charts 1-250, experiences 1-200.
|
|
272
|
+
--offset <number> charts only.
|
|
273
|
+
--cursor <cursor> experiences only. Continuation token.
|
|
274
|
+
|
|
275
|
+
A Roblox universe id is NOT a Steam appid. Ids travel as decimal strings
|
|
276
|
+
because they exceed the range a JSON number represents exactly, and nothing
|
|
277
|
+
here merges a Roblox experience with a Steam game.
|
|
278
|
+
|
|
279
|
+
Unknown is never zero. A null player count was not measured. price.known
|
|
280
|
+
false means the price is UNKNOWN, not that the experience is free. Zero
|
|
281
|
+
recorded votes gives no positive percentage at all, rather than 0%.
|
|
282
|
+
|
|
283
|
+
Nothing is fabricated to fill a gap. An absent cohort answers availability
|
|
284
|
+
"never_observed" with an empty entries list — a measurement about
|
|
285
|
+
collection, not an error and never another sort's ranking. History reports
|
|
286
|
+
unsampled periods in data.gaps and interpolates nothing, so a gap is not a
|
|
287
|
+
zero. Daily sampledPlayingMin/Max are the extremes of the SAMPLES, not the
|
|
288
|
+
day's peak; compare CCU only across points whose comparable is true.
|
|
289
|
+
Observed counter decreases are reported as-is and are not resets.
|
|
290
|
+
|
|
291
|
+
This is a curated tracked cohort, never all of Roblox. Quote
|
|
292
|
+
coverage.trackedUniverses as the denominator for any share.
|
|
293
|
+
|
|
294
|
+
While the surface is switched off the commands answer 404
|
|
295
|
+
ROBLOX_UNAVAILABLE. That is the documented not-available state, not an
|
|
296
|
+
outage and not worth retrying on a schedule.
|
|
297
|
+
|
|
298
|
+
Examples:
|
|
299
|
+
gmc roblox coverage --json
|
|
300
|
+
gmc roblox charts --country JP --sort top-trending --json
|
|
301
|
+
gmc roblox experiences --min-player-count 1000 --order-by player_count --json
|
|
302
|
+
gmc roblox history <universeId> --granularity hourly --json
|
|
303
|
+
|
|
209
304
|
Binary aliases:
|
|
210
305
|
gmcp and game-market-copilot run the same CLI. Use them when a shell
|
|
211
306
|
alias shadows gmc (Oh My Zsh's git plugin defines gmc as
|
|
@@ -306,6 +401,10 @@ Filters:
|
|
|
306
401
|
result carries catalog_as_of_filter_unavailable
|
|
307
402
|
instead: there is no availability history behind
|
|
308
403
|
fixtures, so the filter was not applied at all.
|
|
404
|
+
--release-status <value> released, upcoming, or tba. An exact match on
|
|
405
|
+
top of the guard that already drops any title
|
|
406
|
+
still marked upcoming after its provisional
|
|
407
|
+
release date has passed, on every query.
|
|
309
408
|
--ai-disclosure <value> disclosed, absent, or unconfirmed. absent means
|
|
310
409
|
no AI-use disclosure was observed in collected
|
|
311
410
|
data -- not confirmed non-use. unconfirmed means
|
package/src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import { matches, parseArgs } from "./args.js";
|
|
|
10
10
|
import { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
|
|
11
11
|
import { printGamesSearchHelp, printHelp } from "./help.js";
|
|
12
12
|
import { writeJsonError } from "./output.js";
|
|
13
|
-
import { applyOutputProjection } from "./projection.js";
|
|
13
|
+
import { applyOutputProjection, assertPresetApplicable } from "./projection.js";
|
|
14
14
|
import { buildRequest } from "./requests.js";
|
|
15
15
|
import { trackCliCommandExecuted } from "./telemetry.js";
|
|
16
16
|
import { executeRequestPlan } from "./workflows.js";
|
|
@@ -71,6 +71,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
71
71
|
process.exit(EXIT_USAGE);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// NAK-804 cross-model review: validated BEFORE the metered request below,
|
|
75
|
+
// not only afterward inside applyOutputProjection — an inapplicable
|
|
76
|
+
// --preset must never pay for the HTTP round trip (or its credit charge)
|
|
77
|
+
// only to be rejected once the response is already back.
|
|
78
|
+
assertPresetApplicable({ fields: options.fields, preset: options.preset, request });
|
|
79
|
+
|
|
74
80
|
if (request.requiresApiKey && !apiKey) {
|
|
75
81
|
writeJsonError({
|
|
76
82
|
code: "CLI_MISSING_CONFIG",
|
package/src/main.test.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { main } from "./main.js";
|
|
5
|
+
import { assertPresetApplicable } from "./projection.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Async counterpart to the `captureUsageFailure` helper in requests.test.js:
|
|
9
|
+
* `main()` is async, so `process.exit` must be mocked to throw INTO the
|
|
10
|
+
* awaited call rather than a synchronous one.
|
|
11
|
+
*/
|
|
12
|
+
async function captureCliExit(run) {
|
|
13
|
+
const originalWrite = process.stdout.write;
|
|
14
|
+
const originalExit = process.exit;
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let exitCode;
|
|
17
|
+
process.stdout.write = (chunk) => {
|
|
18
|
+
chunks.push(String(chunk));
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
process.exit = (code) => {
|
|
22
|
+
exitCode = code;
|
|
23
|
+
throw new Error("__cli_exit__");
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
await run();
|
|
27
|
+
throw new Error("expected the CLI to exit");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.message !== "__cli_exit__") throw error;
|
|
30
|
+
} finally {
|
|
31
|
+
process.stdout.write = originalWrite;
|
|
32
|
+
process.exit = originalExit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { output: JSON.parse(chunks.join("")), exitCode };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* NAK-804 cross-model review (Codex Sol): `gmc games ai-disclosure 730
|
|
40
|
+
* --preset summary --json` used to build the request, send it, and only
|
|
41
|
+
* THEN discover in applyOutputProjection that ai-disclosure supports no
|
|
42
|
+
* preset — charging a credit for a call whose answer was always going to be
|
|
43
|
+
* a usage error. The counting fetch stub below is the direct evidence that
|
|
44
|
+
* the metered request never fires: if the ordering regresses, this test
|
|
45
|
+
* fails on `fetchCalls === 0` even though the exit code/message stay right.
|
|
46
|
+
*/
|
|
47
|
+
test("(NAK-804) an unsupported --preset is rejected before the metered request executes", async () => {
|
|
48
|
+
const originalFetch = globalThis.fetch;
|
|
49
|
+
let fetchCalls = 0;
|
|
50
|
+
globalThis.fetch = async () => {
|
|
51
|
+
fetchCalls += 1;
|
|
52
|
+
throw new Error("fetch must not be called once --preset is rejected");
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const { output, exitCode } = await captureCliExit(() =>
|
|
57
|
+
main(["games", "ai-disclosure", "730", "--preset", "summary", "--json"])
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
assert.equal(fetchCalls, 0);
|
|
61
|
+
assert.equal(exitCode, 2);
|
|
62
|
+
assert.equal(output.error.code, "CLI_USAGE_ERROR");
|
|
63
|
+
assert.match(
|
|
64
|
+
output.error.message,
|
|
65
|
+
/--preset summary is supported for gmc reviews patterns, gmc games benchmark, and gmc cohort\/pattern workflows/
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
globalThis.fetch = originalFetch;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The same early check is generic (it reuses resolvePresetFields exactly),
|
|
74
|
+
* not an ai-disclosure special case: an unknown preset NAME is rejected
|
|
75
|
+
* before the request too, for any command.
|
|
76
|
+
*/
|
|
77
|
+
test("(NAK-804) an unknown --preset value is rejected before the metered request executes", async () => {
|
|
78
|
+
const originalFetch = globalThis.fetch;
|
|
79
|
+
let fetchCalls = 0;
|
|
80
|
+
globalThis.fetch = async () => {
|
|
81
|
+
fetchCalls += 1;
|
|
82
|
+
throw new Error("fetch must not be called once --preset is rejected");
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { output, exitCode } = await captureCliExit(() =>
|
|
87
|
+
main(["games", "ai-disclosure", "730", "--preset", "not-a-real-preset", "--json"])
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assert.equal(fetchCalls, 0);
|
|
91
|
+
assert.equal(exitCode, 2);
|
|
92
|
+
assert.equal(output.error.code, "CLI_USAGE_ERROR");
|
|
93
|
+
assert.match(output.error.message, /--preset must be one of: summary/);
|
|
94
|
+
} finally {
|
|
95
|
+
globalThis.fetch = originalFetch;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Negative control on the pure check itself (no CLI/network plumbing
|
|
101
|
+
* needed): a request `kind` that IS in the preset allowlist must not be
|
|
102
|
+
* rejected — the early check only intercepts INAPPLICABLE presets, not
|
|
103
|
+
* every --preset call.
|
|
104
|
+
*/
|
|
105
|
+
test("(NAK-804) assertPresetApplicable does not reject a preset the request kind supports", () => {
|
|
106
|
+
assert.doesNotThrow(() =>
|
|
107
|
+
assertPresetApplicable({ preset: "summary", request: { kind: "reviewPatterns" } })
|
|
108
|
+
);
|
|
109
|
+
assert.doesNotThrow(() =>
|
|
110
|
+
assertPresetApplicable({ preset: "summary", request: { kind: "benchmark" } })
|
|
111
|
+
);
|
|
112
|
+
assert.doesNotThrow(() =>
|
|
113
|
+
assertPresetApplicable({ preset: "summary", request: { kind: "cohortEvidence" } })
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Same precedence rule applyOutputProjection enforces: --fields silences an
|
|
119
|
+
* otherwise-inapplicable --preset entirely, so passing both together must
|
|
120
|
+
* not trip the early check either.
|
|
121
|
+
*/
|
|
122
|
+
test("(NAK-804) assertPresetApplicable defers to --fields precedence, like applyOutputProjection", () => {
|
|
123
|
+
assert.doesNotThrow(() =>
|
|
124
|
+
assertPresetApplicable({
|
|
125
|
+
fields: "state",
|
|
126
|
+
preset: "summary",
|
|
127
|
+
request: { kind: undefined }
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
});
|
package/src/projection.js
CHANGED
|
@@ -47,6 +47,27 @@ export function applyOutputProjection(payload, { fields, preset, request } = {})
|
|
|
47
47
|
return applyFieldsProjection(payload, presetFields, `preset ${preset}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* NAK-804 cross-model review (Codex Sol): every metered command built a
|
|
52
|
+
* usage-error-only exit on an inapplicable --preset, but the check ran
|
|
53
|
+
* INSIDE applyOutputProjection, which main() calls only AFTER the HTTP
|
|
54
|
+
* request already executed (and, for a metered endpoint, already charged a
|
|
55
|
+
* credit). This is the same validation, called right after buildRequest()
|
|
56
|
+
* and before that request is ever sent, so the usage error lands before any
|
|
57
|
+
* network call or charge. General fix, not ai-disclosure-specific: it reuses
|
|
58
|
+
* resolvePresetFields exactly, so any command missing from its
|
|
59
|
+
* kind-allowlist gets the early rejection for free.
|
|
60
|
+
*
|
|
61
|
+
* Mirrors applyOutputProjection's own precedence: --fields silences --preset
|
|
62
|
+
* entirely (never even inspected) both here and there, so the two agree on
|
|
63
|
+
* every input rather than only on the paths that reach a real request.
|
|
64
|
+
*/
|
|
65
|
+
export function assertPresetApplicable({ fields, preset, request } = {}) {
|
|
66
|
+
if (fields != null && fields !== "") return;
|
|
67
|
+
if (preset == null || preset === "") return;
|
|
68
|
+
resolvePresetFields(preset, request);
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
function resolvePresetFields(preset, request) {
|
|
51
72
|
if (preset !== "summary") {
|
|
52
73
|
writeJsonError({
|