@witchpot/gmc 0.9.0 → 0.11.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 CHANGED
@@ -2,6 +2,88 @@
2
2
 
3
3
  All notable changes to the Game Market Copilot CLI will be recorded here.
4
4
 
5
+ ## 0.11.0 - 2026-10-27
6
+
7
+ - `gmc games follower-history <appid>`: observed Steam follower series (same window flags as `price-history`). MCP: `game_follower_history`.
8
+ - `gmc games events <appid> [--include rank_series]`: Steam event participations with observed ranks and shelves. MCP: `game_steam_events`.
9
+ - The bundled `gmc-analysis` skill 0.17.0 covers both commands, and the
10
+ optional `positiveReviews` field on `gmc games review-history` points (state
11
+ a rate as `positiveReviews / totalReviews` when it is present).
12
+
13
+ - New `gmc games price-history <appid>` and `gmc games screenshot-labels
14
+ <appid>` commands (NAK-934, Provisional), the same plain-appid shape as
15
+ `gmc games ai-disclosure`. `price-history` returns the observed Steam
16
+ storefront price on every observed day (major currency units, with
17
+ discount, currency and store country) and takes `--range 7d|1m|1y|all`
18
+ (default `all`) or `--from`/`--to` (`YYYY-MM-DD`, real calendar dates,
19
+ `--from` on or before `--to`), never both; a bad window is a usage error
20
+ before any request. Days without an observation
21
+ are absent, never filled, and a price never implies sales.
22
+ `screenshot-labels` returns one set of visual labels per Steam store
23
+ screenshot and takes no flags. The labels are model output, not Steam
24
+ data. `docs/cli-use-cases.md` and `docs/agent-usage.md` cover both.
25
+
26
+ - Not shipped here: the bundled `gmc-analysis` skill does not mention the
27
+ two new commands yet. It is a separately versioned artifact with its own
28
+ manifest gate, so its coverage follows in its own change.
29
+
30
+ - New `gmc roblox saved | save | remove` commands (NAK-723, Provisional),
31
+ mapping the saved-experiences surface 0.10.0 recorded as "not yet
32
+ mapped". `gmc roblox saved --json` lists the workspace's saved
33
+ experiences resolved for one chart, country and cutoff (`--country`,
34
+ `--device`, `--sort`, `--as-of`, `--eligibility-days`); `gmc roblox save
35
+ <universeId>` and `gmc roblox remove <universeId>` change the set and
36
+ take no flags at all, refusing a scope flag rather than ignoring it.
37
+
38
+ Unlike the five read commands these are workspace-scoped: they need an
39
+ API key or CLI credential and answer 401 without one, which is not an
40
+ empty saved list. They charge no credits — saving is workspace metadata,
41
+ not analytics — and the paid-plan gate still applies.
42
+
43
+ Saved membership is scope-independent: the flags on `saved` decide which
44
+ of `in_scope` / `out_of_scope` / `not_tracked` each row is in, never
45
+ which rows are listed. Both writes are idempotent and report the state
46
+ that now holds, so `changed: false` is success rather than a write to
47
+ retry. Universe ids travel as decimal strings into the request body as
48
+ well as the path, and the CLI rejects a spelling the server would reject
49
+ before any request is sent.
50
+
51
+ `docs/cli-use-cases.md` (new case 70) and `docs/agent-usage.md` now cover
52
+ the saved commands alongside the read family.
53
+
54
+ - The 0.10.0 entry below is left exactly as published. Its "not yet
55
+ mapped" line was true of 0.10.0 and is corrected by this entry rather
56
+ than edited in place, matching how 0.8.0 recorded the 0.7.0 registry gap:
57
+ a published entry describes the release users installed, so amending one
58
+ would make the repo disagree with the tarball already on npm.
59
+
60
+ - Not shipped here: the bundled `gmc-analysis` skill still documents the
61
+ Roblox read family only. The skill is a separately versioned artifact
62
+ with its own manifest gate, so its Roblox-saved coverage and version bump
63
+ follow in their own change.
64
+
65
+ ## 0.10.0 - 2026-09-22
66
+
67
+ - New `gmc roblox` command family (NAK-715, Provisional): `charts` (one
68
+ recorded ranking cohort as of a cutoff), `experiences` (the tracked
69
+ catalog with filters, sorts and signed-cursor pagination), `experience
70
+ <universeId>` (one experience with per-metric observation times),
71
+ `history <universeId> --granularity hourly|daily` (retained observations
72
+ with explicit gaps), and `coverage` (the configured cohort matrix with
73
+ per-dimension freshness). Universe ids are decimal strings end to end —
74
+ they are never Steam appids, and the commands reject one. The Roblox
75
+ surface went live in production on 2026-09-22 (NAK-725); the commands
76
+ stay Provisional until the surface has been through a broader real-data
77
+ smoke period.
78
+
79
+ - The Roblox read endpoints reject impossible calendar dates in every
80
+ offset spelling (`as_of=2026-02-31T12:00:00+09:00` is now a 400 naming
81
+ the nonexistent date instead of silently answering for March 3), and
82
+ history gap counts always reconcile with the expected point count.
83
+
84
+ - Not yet mapped: the saved-experiences surface (`/api/v1/roblox/saved`,
85
+ NAK-723) is live over REST and MCP; a CLI mapping will follow.
86
+
5
87
  ## 0.9.0 - 2026-09-16
6
88
 
7
89
  - New `--release-status <released|upcoming|tba>` filter on the games
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchpot/gmc",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "description": "Command line tools for Game Market Copilot market research.",
6
6
  "license": "UNLICENSED",
@@ -1,5 +1,27 @@
1
1
  # gmc-analysis skill changelog
2
2
 
3
+ ## 0.17.0 - 2026-10-08
4
+
5
+ - Review-count history (NAK-934): each `series[]` point can carry the
6
+ optional `positiveReviews`, the positive-review count observed on the same
7
+ row, beside the optional `ratingPercent`. Both are absent, never zero, when
8
+ the row carried no usable value. New guardrail: state a rate as
9
+ `positiveReviews / totalReviews` when the count is present, because
10
+ `ratingPercent` is rounded to one decimal (1,999 of 2,000 reads 100.0);
11
+ without the count, a `100.0` on 2,000 or more reviews is possibly rounded
12
+ and is never reported as "no negative reviews".
13
+ - New "Steam follower history" section: `gmc games follower-history <appid>`
14
+ (MCP: `game_follower_history`), its `--range` or `--from`/`--to` window
15
+ (never both), and the guardrails: observed days only, followers are not
16
+ wishlists or sales, `not_collected` is a collection state, truncation
17
+ drops the oldest points.
18
+ - New "Steam event participation" section: `gmc games events <appid>
19
+ [--include rank_series]` (MCP: `game_steam_events`), with the guardrails:
20
+ ranks and shelves are observed event-page state, not an official ranking;
21
+ an absent block or an empty list is a collection state; the rank series is
22
+ raw and never resampled.
23
+ - MCP-only mapping updated with both tools.
24
+
3
25
  ## 0.16.0 - 2026-09-16
4
26
 
5
27
  - New "Disclosure text and change history" subsection under "AI disclosure
@@ -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.16.0
11
+ version: 0.17.0
12
12
  ---
13
13
 
14
14
  # GMC Analysis
@@ -59,7 +59,9 @@ applies identically. Mechanics map as follows:
59
59
  its lifecycle/announcement/discount context (`games review-history`) ->
60
60
  `game_review_history`; per-title AI-disclosure text and change history
61
61
  (`games ai-disclosure`) -> `game_profile` with `ai_disclosure` in
62
- `sections`.
62
+ `sections`; Steam follower history (`games follower-history`) ->
63
+ `game_follower_history`; Steam event participation (`games events`) ->
64
+ `game_steam_events` (`include: ["rank_series"]` for the rank series).
63
65
  - Per-family collection status ("has GMC actually collected X for this
64
66
  game?") has no CLI command yet and is MCP-only: `game_collection_status`
65
67
  (see the dedicated section below).
@@ -413,6 +415,13 @@ Field paths differ by surface: the CLI and REST wrap the payload, so read
413
415
  the same fields at the top level. The guardrails below name the CLI path;
414
416
  drop the `data.` prefix over MCP.
415
417
 
418
+ Each `data.series[]` point carries `date` and `totalReviews`, and two
419
+ OPTIONAL fields: `ratingPercent` (the positive-review percentage observed on
420
+ that row, 0-100, rounded to one decimal) and `positiveReviews` (the
421
+ positive-review count observed on that row, 0 to `totalReviews`). Absent
422
+ means the row carried no usable value; neither is ever zero-filled, and a
423
+ returned `0` is a real observation.
424
+
416
425
  Interpretation guardrails (honesty rules, not suggestions):
417
426
 
418
427
  - **Scale by date, never by array index.** Collection has real gaps, so two
@@ -439,9 +448,67 @@ Interpretation guardrails (honesty rules, not suggestions):
439
448
  day from it. `endDate: null` means no end has been observed yet;
440
449
  `discountPercent` is the opening observation, not the deepest or final
441
450
  depth.
451
+ - **State a rate from the count when it is there.** `ratingPercent` is
452
+ rounded to one decimal, so 1,999 positive of 2,000 reads `100.0`. When
453
+ `positiveReviews` is present, compute the rate as
454
+ `positiveReviews / totalReviews`. When it is absent and `ratingPercent` is
455
+ `100.0` on `totalReviews` of 2,000 or more, treat the 100 as possibly
456
+ rounded (anything from 99.95% up): say "about 100% (rounded)", never "no
457
+ negative reviews". Below 2,000 reviews a `100.0` is exact, because one
458
+ negative review already reads 99.9 or less.
442
459
  - **Temporal comparison only.** "Reviews rose in the week after the update"
443
460
  is a claim you can make; "the update caused the rise" is not.
444
461
 
462
+ ## Steam follower history
463
+
464
+ `gmc games follower-history <appid> [--range 7d|1m|1y|all] [--from YYYY-MM-DD] [--to YYYY-MM-DD] --json`
465
+ (MCP: `game_follower_history` with `appid` and either `range` or
466
+ `from`/`to`) returns one game's Steam Community group member count on every
467
+ OBSERVED day: `data.series[]` of `date` and `followerCount`, plus
468
+ `data.availability` and `data.seriesMeta`. It takes the appid alone (no
469
+ `--source`). `--range` defaults to `all`, ending today (UTC); `--from` and
470
+ `--to` are inclusive. `--range` cannot be combined with `--from`/`--to`:
471
+ the CLI rejects that as a usage error before any request, so the failure is
472
+ not transient.
473
+
474
+ Interpretation guardrails:
475
+
476
+ - **Observed days only.** Days without an observation are absent and never
477
+ filled or interpolated. Scale by date, never by array index.
478
+ - **Followers are not wishlists.** They are Steam Community game-group
479
+ members, a public proxy for audience interest. Never report them as a
480
+ wishlist count and never as sales.
481
+ - **`not_collected` is a collection state.** `data.availability:
482
+ not_collected` means followers were never collected, never zero followers.
483
+ - **Truncation drops the oldest points.** When `data.seriesMeta.truncated`
484
+ is true the series still ends at the newest observation; say the early
485
+ history is cut.
486
+
487
+ ## Steam event participation
488
+
489
+ `gmc games events <appid> [--include rank_series] --json` (MCP:
490
+ `game_steam_events`, with `include: ["rank_series"]`) returns the
491
+ Steam-run events the game took part in (Next Fest and others), newest
492
+ first: each with its observed best rank, shelf exposure, demo review
493
+ series, store follower series and social account deltas. `--include
494
+ rank_series` adds `rankSeries[]` of `{ snapshotUtc, rank, sourceKey }` per
495
+ event, bounded per event and per response (`rankSeriesTruncated` marks an
496
+ event a cap cut).
497
+
498
+ Interpretation guardrails:
499
+
500
+ - **Observed state, not an official ranking.** Ranks and shelf values are
501
+ event-page state captured by the collector. Report them as observed
502
+ positions, never as Steam's own ranking.
503
+ - **Absence is a collection state.** An absent block means that signal was
504
+ not collected for the event. An empty `data.events` means no observed
505
+ participation, not proof the game never took part.
506
+ - **The rank series is raw.** `rankSeries` holds the observations themselves
507
+ (about every 30 minutes during a festival), never resampled; a gap is a
508
+ real collection gap.
509
+ - **Temporal comparison only.** A follower or review change during an event
510
+ is something you observed beside it, never something the event caused.
511
+
445
512
  ## Per-family collection status
446
513
 
447
514
  `game_collection_status` (MCP-only, no CLI command) returns per-signal-family
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.16.0",
3
- "contentHash": "a751df9637a24adb16604c91f77618ba1af16ce191843d85d3d8f2e6ae4df363",
2
+ "version": "0.17.0",
3
+ "contentHash": "64d3e4ed54842f921cc532ac76c936fbd99619ceed13dc6fc9317fe56552df2d",
4
4
  "files": 7
5
5
  }
package/src/args.js CHANGED
@@ -21,6 +21,12 @@ export function parseArgs(args) {
21
21
  const command = [];
22
22
  const positional = [];
23
23
  const options = {};
24
+ // Option names given more than once. Later values still win in `options`
25
+ // (every command's parsing is unchanged); a command that cannot take a list
26
+ // reads this to reject the repetition. Non-enumerable so it never shows up
27
+ // as an option.
28
+ const repeated = new Set();
29
+ Object.defineProperty(options, "repeatedOptions", { value: repeated, enumerable: false });
24
30
 
25
31
  for (let index = 0; index < args.length; index += 1) {
26
32
  const arg = args[index];
@@ -57,6 +63,7 @@ export function parseArgs(args) {
57
63
  }
58
64
 
59
65
  if (inlineValue == null) index += 1;
66
+ if (Object.prototype.hasOwnProperty.call(options, key)) repeated.add(key);
60
67
  options[key] = value;
61
68
  continue;
62
69
  }
@@ -90,6 +97,11 @@ function isCommandToken(command, token) {
90
97
  // unknown command `steam` with a stray positional rather than reaching
91
98
  // the tag-trends branch.
92
99
  "steam",
100
+ // NAK-715. Same trap as `steam` above: a token missing from this list
101
+ // falls through to positional rather than erroring, so `gmc roblox
102
+ // charts` would parse as the unknown command `roblox` with a stray
103
+ // positional instead of reaching the charts branch.
104
+ "roblox",
93
105
  "auth",
94
106
  "login",
95
107
  "logout",
@@ -109,7 +121,7 @@ function isCommandToken(command, token) {
109
121
  return ["install", "print", "status"].includes(token);
110
122
  }
111
123
  if (command[0] === "games") {
112
- return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this", "ai-disclosure"].includes(token);
124
+ return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this", "ai-disclosure", "price-history", "follower-history", "events", "screenshot-labels"].includes(token);
113
125
  }
114
126
  if (command[0] === "reviews") {
115
127
  return token === "patterns";
@@ -138,6 +150,23 @@ function isCommandToken(command, token) {
138
150
  if (command[0] === "steam") {
139
151
  return ["tag-trends", "trend-stories"].includes(token);
140
152
  }
153
+ if (command[0] === "roblox") {
154
+ // NAK-723 adds saved/save/remove. Same trap the read family documents, and
155
+ // worse for the two writes: a token missing from this list falls through to
156
+ // positional rather than erroring, so `gmc roblox save <universeId>` would
157
+ // parse as the unknown command `roblox` and exit 2 with a message naming
158
+ // the id rather than the subcommand.
159
+ return [
160
+ "charts",
161
+ "experiences",
162
+ "experience",
163
+ "history",
164
+ "coverage",
165
+ "saved",
166
+ "save",
167
+ "remove"
168
+ ].includes(token);
169
+ }
141
170
  return false;
142
171
  }
143
172
 
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.9.0";
10
+ export const CLI_VERSION = "0.11.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");
@@ -63,7 +63,28 @@ export const PROVISIONAL_BETA_COMMANDS = [
63
63
  "gmc steam tag-trends --view featured|board|group|table|tag --json",
64
64
  // NAK-804. New command; starts Provisional per the closed-beta roadmap's
65
65
  // default for anything not yet through broader real-data smoke testing.
66
- "gmc games ai-disclosure <appid> --json"
66
+ "gmc games ai-disclosure <appid> --json",
67
+ // NAK-934. New commands; Provisional per the same closed-beta default.
68
+ "gmc games price-history <appid> --range 7d|1m|1y|all --json",
69
+ "gmc games follower-history <appid> --range 7d|1m|1y|all --json",
70
+ "gmc games events <appid> --include rank_series --json",
71
+ "gmc games screenshot-labels <appid> --json",
72
+ // NAK-715/NAK-725. Enabled in production on 2026-09-22; stays Provisional
73
+ // per the closed-beta roadmap's default until the surface has been through
74
+ // a broader real-data smoke period (the NAK-804 rule), then graduates.
75
+ "gmc roblox charts --country all --sort top-playing-now --json",
76
+ "gmc roblox experiences --country all --limit 50 --json",
77
+ "gmc roblox experience <universeId> --json",
78
+ "gmc roblox history <universeId> --granularity daily|hourly --json",
79
+ "gmc roblox coverage --json",
80
+ // NAK-723. The workspace's saved Roblox experiences, mapping the REST
81
+ // surface that shipped with the pages. Provisional for the SAME reason the
82
+ // five read commands above are — they are one surface, gated by one flag,
83
+ // and graduating the saved commands ahead of the reads they resolve against
84
+ // would promise a stability the reads have not earned yet.
85
+ "gmc roblox saved --country all --json",
86
+ "gmc roblox save <universeId> --json",
87
+ "gmc roblox remove <universeId> --json"
67
88
  ];
68
89
 
69
90
  export const COMPARE_FOCUSES = ["overview", "market", "reviews", "marketing", "audience", "showcases"];
package/src/help.js CHANGED
@@ -52,6 +52,10 @@ Usage:
52
52
  gmc games review-history --source steam <externalId> --json
53
53
  gmc games more-like-this --source steam <externalId> --country <country> --json
54
54
  gmc games ai-disclosure <appid> --json
55
+ gmc games price-history <appid> --range 1y --json
56
+ gmc games follower-history <appid> --range 1y --json
57
+ gmc games events <appid> --json
58
+ gmc games screenshot-labels <appid> --json
55
59
  gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
56
60
  gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --sample-size <number> --preset summary --json
57
61
  gmc marketing patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
@@ -70,6 +74,14 @@ Usage:
70
74
  gmc steam tag-trends --view featured --json
71
75
  gmc steam tag-trends --view table --sort release_share_delta --order desc --json
72
76
  gmc steam tag-trends --view tag --tag <tag_slug> --json
77
+ gmc roblox charts --country all --sort top-playing-now --json
78
+ gmc roblox experiences --country all --limit 50 --json
79
+ gmc roblox experience <universeId> --json
80
+ gmc roblox history <universeId> --granularity daily --json
81
+ gmc roblox coverage --json
82
+ gmc roblox saved --country all --json
83
+ gmc roblox save <universeId> --json
84
+ gmc roblox remove <universeId> --json
73
85
 
74
86
  Tag taxonomy:
75
87
  gmc tags --json List the full tag dictionary (slug, name).
@@ -177,6 +189,77 @@ AI-disclosure text and history:
177
189
  Example:
178
190
  gmc games ai-disclosure 730 --json
179
191
 
192
+ Observed price history:
193
+ gmc games price-history <appid> [--range <preset>] [--from <date>] [--to <date>] --json
194
+ One game's Steam storefront price on
195
+ every OBSERVED day: priceInitial,
196
+ priceFinal (major currency units),
197
+ discountPercent, currency, storeCountry.
198
+ --range <preset> 7d | 1m | 1y | all, ending today (UTC).
199
+ Defaults to all.
200
+ --from <YYYY-MM-DD> Inclusive window start.
201
+ --to <YYYY-MM-DD> Inclusive window end; defaults to today.
202
+ --range cannot be combined with --from
203
+ or --to.
204
+
205
+ Collection is not daily for every game, so days without an observation are
206
+ absent and never filled; scale by date, never by array index. priceFinal 0
207
+ with a null priceInitial is a free game; a null priceFinal means no price
208
+ was shown that day. data.availability not_collected means no snapshot was
209
+ ever collected, never a free game. A price says nothing about units sold or
210
+ revenue.
211
+
212
+ Example:
213
+ gmc games price-history 730 --range 1y --json
214
+
215
+ Steam follower history:
216
+ gmc games follower-history <appid> [--range <preset>] [--from <date>] [--to <date>] --json
217
+ Steam Community game-group members on
218
+ every OBSERVED day: date, followerCount.
219
+ --range <preset> 7d | 1m | 1y | all, ending today (UTC).
220
+ Defaults to all. Cannot be combined with
221
+ --from or --to.
222
+
223
+ Followers are a public proxy for audience interest, never a wishlist count
224
+ and never sales. Days without an observation are absent and never filled.
225
+ data.availability not_collected means followers were never collected.
226
+
227
+ Example:
228
+ gmc games follower-history 730 --range 1y --json
229
+
230
+ Steam event participation:
231
+ gmc games events <appid> [--include rank_series] --json
232
+ Steam-run events the game took part in
233
+ (Next Fest and others), newest first, with
234
+ observed best rank, shelf exposure, demo
235
+ reviews, store followers and social
236
+ deltas.
237
+ --include rank_series Attach the observed ranking series per
238
+ event.
239
+
240
+ Ranks and shelves are observed event-page state, never an official Steam
241
+ ranking. An absent block means the signal was not collected.
242
+
243
+ Example:
244
+ gmc games events 730 --include rank_series --json
245
+
246
+ Screenshot labels:
247
+ gmc games screenshot-labels <appid> --json
248
+ Visual labels for each Steam store
249
+ screenshot of one game (screen type,
250
+ camera, art style, lighting, colour, a
251
+ one-line observation), in store order.
252
+ There are no parameters beyond the appid.
253
+
254
+ The labels are MODEL OUTPUT (data.labels[].labeler), not Steam data and not
255
+ a human review, and they can be wrong. One label set is returned per
256
+ screenshot: the most recently labelled taxonomy version. data.availability
257
+ not_labelled means nothing has been labelled yet, never that the game has
258
+ no screenshots.
259
+
260
+ Example:
261
+ gmc games screenshot-labels 730 --json
262
+
180
263
  Steam tag trends:
181
264
  gmc steam tag-trends --view <view> --json
182
265
  Tag supply trends over one frozen monthly
@@ -232,6 +315,103 @@ Steam tag trends:
232
315
  gmc steam tag-trends --view table --sort current_share --limit 50 --json
233
316
  gmc steam tag-trends --view tag --tag cozy --json
234
317
 
318
+ Roblox:
319
+ gmc roblox charts --json One ranking cohort's stored chart
320
+ snapshot as of a cutoff.
321
+ gmc roblox experiences --json Browse the tracked cohort with coverage.
322
+ gmc roblox experience <universeId> --json
323
+ One experience: metadata, metrics,
324
+ artwork.
325
+ gmc roblox history <universeId> --json
326
+ A bounded window of stored metric
327
+ observations.
328
+ gmc roblox coverage --json What the surface has, per dimension.
329
+ gmc roblox saved --json This WORKSPACE's saved experiences,
330
+ resolved for one cohort and cutoff.
331
+ gmc roblox save <universeId> --json
332
+ Save one experience to the workspace.
333
+ gmc roblox remove <universeId> --json
334
+ Remove one from the workspace's saved
335
+ set.
336
+ --country <country> all | US | JP. Selects which CHART was
337
+ read, never a regional metric scope.
338
+ --device <device> computer.
339
+ --sort <sort> top-playing-now | top-trending |
340
+ top-revisited | fun-with-friends.
341
+ --as-of <instant> ISO 8601 cutoff; defaults to now. No
342
+ observation after it is considered.
343
+ --eligibility-days <number> experiences and saved, 1-30 (default 7).
344
+ How far back a chart appearance counts.
345
+ --min-player-count <number> experiences only, on the resolved row.
346
+ --max-rank <number> experiences only, on the resolved row.
347
+ --genre <genre> experiences only, exact match.
348
+ --creator-type <type> experiences only, e.g. Group or User.
349
+ --detail <enriched|chart_only> experiences only. Omitted returns both.
350
+ --order-by <key> rank | player_count | last_chart_seen |
351
+ universe_id (experiences only).
352
+ --order <asc|desc> experiences only.
353
+ --granularity <hourly|daily> history only. Defaults to daily.
354
+ --from <instant> --to <instant> history only. Hourly windows span at most
355
+ 90 days, daily at most 365.
356
+ --limit <number> charts 1-250, experiences 1-200.
357
+ --offset <number> charts only.
358
+ --cursor <cursor> experiences only. Continuation token.
359
+
360
+ A Roblox universe id is NOT a Steam appid. Ids travel as decimal strings
361
+ because they exceed the range a JSON number represents exactly, and nothing
362
+ here merges a Roblox experience with a Steam game.
363
+
364
+ Unknown is never zero. A null player count was not measured. price.known
365
+ false means the price is UNKNOWN, not that the experience is free. Zero
366
+ recorded votes gives no positive percentage at all, rather than 0%.
367
+
368
+ Nothing is fabricated to fill a gap. An absent cohort answers availability
369
+ "never_observed" with an empty entries list — a measurement about
370
+ collection, not an error and never another sort's ranking. History reports
371
+ unsampled periods in data.gaps and interpolates nothing, so a gap is not a
372
+ zero. Daily sampledPlayingMin/Max are the extremes of the SAMPLES, not the
373
+ day's peak; compare CCU only across points whose comparable is true.
374
+ Observed counter decreases are reported as-is and are not resets.
375
+
376
+ This is a curated tracked cohort, never all of Roblox. Quote
377
+ coverage.trackedUniverses as the denominator for any share.
378
+
379
+ While the surface is switched off the commands answer 404
380
+ ROBLOX_UNAVAILABLE. That is the documented not-available state, not an
381
+ outage and not worth retrying on a schedule.
382
+
383
+ Saved experiences (workspace-scoped, unlike the five reads above):
384
+ saved, save and remove carry one workspace's own data and need a
385
+ credential — a Game Market Copilot API key or a CLI credential. Without
386
+ one they answer 401, which is not an empty saved list.
387
+
388
+ Saved membership is scope-independent. --country, --device, --sort,
389
+ --as-of and --eligibility-days apply to "saved" alone and decide which of
390
+ three states each row is in, never which rows are listed: in_scope
391
+ carries the observation that scope recorded, out_of_scope is saved and
392
+ tracked with NO reading there (never back-filled from another cohort or
393
+ from after the cutoff, and never reportable as zero or as removed), and
394
+ not_tracked has nothing recorded at all. The two writes take no flags.
395
+
396
+ Both writes are idempotent and report the state that NOW holds, so
397
+ changed: false is success and must not be retried as a failure. Saving
398
+ records interest in a universe id — it never triggers collection, however
399
+ often it is called, and it refuses an id the tracked cohort has never
400
+ recorded (404 NOT_FOUND, a different fact from ROBLOX_UNAVAILABLE).
401
+ "remove" removes the id from this workspace's saved set only; it deletes
402
+ no data and removing something not saved is changed: false, not a 404.
403
+ The set is capped per workspace (savedLimit in every response); past it,
404
+ saving answers 409 CONFLICT.
405
+
406
+ Examples:
407
+ gmc roblox coverage --json
408
+ gmc roblox charts --country JP --sort top-trending --json
409
+ gmc roblox experiences --min-player-count 1000 --order-by player_count --json
410
+ gmc roblox history <universeId> --granularity hourly --json
411
+ gmc roblox saved --country JP --sort top-trending --json
412
+ gmc roblox save <universeId> --json
413
+ gmc roblox remove <universeId> --json
414
+
235
415
  Binary aliases:
236
416
  gmcp and game-market-copilot run the same CLI. Use them when a shell
237
417
  alias shadows gmc (Oh My Zsh's git plugin defines gmc as