@witchpot/gmc 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,12 +2,24 @@
2
2
 
3
3
  All notable changes to the Game Market Copilot CLI will be recorded here.
4
4
 
5
- ## 0.1.2 - Unreleased
5
+ ## 0.1.4 - 2026-06-01
6
6
 
7
+ - Reissued the beta CLI package after the count and aggregate API server
8
+ implementation was committed for deployment alignment.
9
+
10
+ ## 0.1.3 - 2026-06-01
11
+
12
+ - Added `gmc games count` for fast Steam catalog counts with bounded filters.
13
+ - Added `gmc games aggregate` for count, average, median, percentile, and bucket
14
+ summaries over Steam catalog cohorts.
15
+ - Added bucket threshold forwarding for price, review, and rating distribution
16
+ workflows.
7
17
  - Fixed the npm executable entrypoint so `gmc` runs correctly through npm,
8
18
  Volta, and other package-manager shims.
9
19
  - Added packed-tarball smoke validation for actual CLI stdout to catch silent
10
20
  executable startup regressions.
21
+ - Improved browser-login token exchange diagnostics for non-JSON Access
22
+ responses and accepted direct or nested credential response envelopes.
11
23
 
12
24
  ## 0.1.1 - 2026-06-01
13
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchpot/gmc",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Command line tools for Game Market Copilot market research.",
6
6
  "license": "UNLICENSED",
package/src/args.js CHANGED
@@ -71,7 +71,7 @@ function isCommandToken(command, token) {
71
71
  return ["get", "set"].includes(token);
72
72
  }
73
73
  if (command[0] === "games") {
74
- return ["search", "resolve", "benchmark", "detail", "analysis", "compare", "topics"].includes(token);
74
+ return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics"].includes(token);
75
75
  }
76
76
  if (command[0] === "reviews") {
77
77
  return token === "patterns";
@@ -88,10 +88,11 @@ export async function exchangeAuthorizationCode({
88
88
  });
89
89
 
90
90
  const contentType = response.headers.get("content-type") ?? "";
91
- const payload = contentType.includes("application/json")
91
+ const isJson = isJsonContentType(contentType);
92
+ const payload = isJson
92
93
  ? await response.json().catch(() => null)
93
94
  : null;
94
- if (!contentType.includes("application/json")) {
95
+ if (!isJson) {
95
96
  writeJsonError({
96
97
  code: "CLI_API_ERROR",
97
98
  message: "GMC CLI authorization exchange returned a non-JSON response. If production is behind Cloudflare Access, allow CLI/API routes or use a Cloudflare service-token path for the CLI.",
@@ -151,6 +152,11 @@ function summarizeTokenExchangeResponse(response) {
151
152
  };
152
153
  }
153
154
 
155
+ export function isJsonContentType(contentType) {
156
+ const mediaType = String(contentType).split(";")[0]?.trim().toLowerCase();
157
+ return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
158
+ }
159
+
154
160
  function getObjectKeys(value) {
155
161
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
156
162
  return Object.keys(value).filter((key) => key !== "secret");
package/src/constants.js CHANGED
@@ -8,7 +8,7 @@ export const EXIT_API = 4;
8
8
 
9
9
  export const CLI_PACKAGE_NAME = "@witchpot/gmc";
10
10
  export const CLI_BETA_PACKAGE_NAME = "@witchpot/gmc";
11
- export const CLI_VERSION = "0.1.2";
11
+ export const CLI_VERSION = "0.1.4";
12
12
 
13
13
  export const DEFAULT_API_BASE_URL = "https://gamemarketcopilot.com";
14
14
  export const DEFAULT_CONFIG_PATH = path.join(homedir(), ".config", "gmc", "config.json");
@@ -20,6 +20,8 @@ export const SUPPORTED_BETA_COMMANDS = [
20
20
  "gmc --version",
21
21
  "gmc update --json",
22
22
  "gmc games search --source steam --query <text> --json",
23
+ "gmc games count --source steam --release-year <year> --reviews-gt <number> --json",
24
+ "gmc games aggregate --source steam --metric count,median_reviews --group-by release_year --json",
23
25
  "gmc games resolve --source steam --query <title> --json",
24
26
  "gmc games detail --source steam <externalId> --json",
25
27
  "gmc games analysis --source steam <externalId> --sections reviews,marketing --json",
package/src/help.js CHANGED
@@ -23,6 +23,8 @@ Usage:
23
23
  gmc config set apiBaseUrl <url>
24
24
  gmc schema --json
25
25
  gmc games search --source steam --query <text> --json
26
+ gmc games count --source steam --release-year <year> --reviews-gt <number> --json
27
+ gmc games aggregate --source steam --metric count,median_reviews --group-by release_year --json
26
28
  gmc games resolve --source steam --query <title> --json
27
29
  gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
28
30
  gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --sample-size <number> --json
@@ -58,3 +60,51 @@ Environment:
58
60
  GMC_CONFIG_PATH Optional JSON config with apiBaseUrl and apiKey
59
61
  `);
60
62
  }
63
+
64
+ export function printGamesSearchHelp() {
65
+ process.stdout.write(`gmc games search
66
+
67
+ Usage:
68
+ gmc games search --source steam [filters] [output options] --json
69
+
70
+ Filters:
71
+ --query <text> Search by title or supported metadata.
72
+ --tags <slug[,slug...]> Filter by Steam tag slugs.
73
+ --tag-mode <and|or> Match all tags or any tag.
74
+ --release-from <date> Inclusive release date lower bound, YYYY-MM-DD.
75
+ --release-to <date> Inclusive release date upper bound, YYYY-MM-DD.
76
+ --release-year <year> Shortcut for that calendar year.
77
+ --reviews-min <number> Inclusive lower bound, reviews >= number.
78
+ --reviews-max <number> Inclusive upper bound, reviews <= number.
79
+ --reviews-gt <number> Strict lower bound, reviews > number.
80
+ --reviews-lt <number> Strict upper bound, reviews < number.
81
+ --rating-min <number> Inclusive rating lower bound, 0-100.
82
+ --price-min <number> Inclusive price lower bound.
83
+ --price-max <number> Inclusive price upper bound.
84
+
85
+ Sorting:
86
+ --sort <key> reviews_desc, reviews_asc, rating_desc,
87
+ rating_asc, release_desc, release_asc,
88
+ price_asc, price_desc, title_asc, title_desc.
89
+
90
+ Pagination:
91
+ --limit <number> 1-100, default 20.
92
+ --offset <number> Offset for the next page.
93
+
94
+ Output:
95
+ --fields <list> Optional comma-separated field list.
96
+ --preset <name> Optional preset when supported.
97
+ --json Required for structured agent workflows.
98
+
99
+ Count and aggregation:
100
+ Use gmc games count when you only need the number of matching games.
101
+ Use gmc games aggregate for grouped metrics such as count by year,
102
+ median reviews by year, or count by an explicit price/review/rating bucket.
103
+
104
+ Examples:
105
+ gmc games search --source steam --release-year 2025 --reviews-min 1000 --json
106
+ gmc games count --source steam --release-year 2025 --reviews-gt 1000 --json
107
+ gmc games aggregate --source steam --metric count,median_reviews --group-by release_year --reviews-gt 1000 --json
108
+ gmc games aggregate --source steam --metric count --group-by price_bucket --bucket price:0,5,10,20,40 --json
109
+ `);
110
+ }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ export {
3
3
  buildLoginUrl,
4
4
  exchangeAuthorizationCode,
5
5
  extractCredentialFromTokenPayload,
6
+ isJsonContentType,
6
7
  runBrowserLogin
7
8
  } from "./browser-login.js";
8
9
  export { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
package/src/main.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./constants.js";
9
9
  import { matches, parseArgs } from "./args.js";
10
10
  import { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
11
- import { printHelp } from "./help.js";
11
+ import { printGamesSearchHelp, printHelp } from "./help.js";
12
12
  import { writeJsonError } from "./output.js";
13
13
  import { applyOutputProjection } from "./projection.js";
14
14
  import { buildRequest } from "./requests.js";
@@ -31,7 +31,16 @@ export async function main(argv = process.argv.slice(2)) {
31
31
  process.exit(0);
32
32
  }
33
33
 
34
- if (options.help || (command.length === 0 && positional.length === 0)) {
34
+ if (options.help) {
35
+ if (matches(command, ["games", "search"])) {
36
+ printGamesSearchHelp();
37
+ } else {
38
+ printHelp();
39
+ }
40
+ process.exit(0);
41
+ }
42
+
43
+ if (command.length === 0 && positional.length === 0) {
35
44
  printHelp();
36
45
  process.exit(0);
37
46
  }
package/src/requests.js CHANGED
@@ -45,6 +45,22 @@ export function buildRequest(command, options, positional) {
45
45
  };
46
46
  }
47
47
 
48
+ if (matches(command, ["games", "count"])) {
49
+ assertSource(options.source);
50
+ return {
51
+ path: "/api/v1/steam/games/count",
52
+ query: buildGameCountQuery(options)
53
+ };
54
+ }
55
+
56
+ if (matches(command, ["games", "aggregate"])) {
57
+ assertSource(options.source);
58
+ return {
59
+ path: "/api/v1/steam/games/aggregate",
60
+ query: buildGameAggregateQuery(options)
61
+ };
62
+ }
63
+
48
64
  if (matches(command, ["games", "resolve"])) {
49
65
  assertSource(options.source);
50
66
  const query = options.query ?? options.q ?? positional.join(" ");
@@ -383,6 +399,20 @@ function pickQuery(options, mapping) {
383
399
  }
384
400
 
385
401
  function buildGameSearchQuery(options) {
402
+ const query = {
403
+ ...buildGameCountQuery(options),
404
+ ...pickQuery(options, {
405
+ sort: "sort",
406
+ limit: "limit",
407
+ offset: "offset",
408
+ locale: "locale"
409
+ })
410
+ };
411
+
412
+ return query;
413
+ }
414
+
415
+ function buildGameCountQuery(options) {
386
416
  const query = pickQuery(options, {
387
417
  query: "q",
388
418
  tags: "tags",
@@ -391,19 +421,72 @@ function buildGameSearchQuery(options) {
391
421
  "release-to": "releaseTo",
392
422
  "reviews-min": "reviewsMin",
393
423
  "reviews-max": "reviewsMax",
424
+ "reviews-gt": "reviewsGt",
425
+ "reviews-lt": "reviewsLt",
394
426
  "rating-min": "ratingMin",
395
427
  "price-min": "priceMin",
396
428
  "price-max": "priceMax",
397
- sort: "sort",
398
- limit: "limit",
399
- offset: "offset",
400
429
  locale: "locale"
401
430
  });
402
431
 
432
+ applyReleaseYearAlias(query, options);
433
+ assertReviewBoundOptions(options);
403
434
  if (query.tags) query.tags = normalizeTagSlugCsv(query.tags);
404
435
  return query;
405
436
  }
406
437
 
438
+ function buildGameAggregateQuery(options) {
439
+ return {
440
+ ...buildGameCountQuery(options),
441
+ ...pickQuery(options, {
442
+ metric: "metric",
443
+ "group-by": "groupBy",
444
+ bucket: "bucket",
445
+ sort: "sort",
446
+ limit: "limit",
447
+ offset: "offset"
448
+ })
449
+ };
450
+ }
451
+
452
+ function applyReleaseYearAlias(query, options) {
453
+ const releaseYear = options["release-year"];
454
+ if (releaseYear == null) return;
455
+ if (!/^\d{4}$/.test(String(releaseYear))) {
456
+ writeJsonError({
457
+ code: "CLI_USAGE_ERROR",
458
+ message: "--release-year must use YYYY"
459
+ });
460
+ process.exit(EXIT_USAGE);
461
+ }
462
+ if (options["release-from"] != null || options["release-to"] != null) {
463
+ writeJsonError({
464
+ code: "CLI_USAGE_ERROR",
465
+ message: "Use either --release-year or --release-from/--release-to, not both"
466
+ });
467
+ process.exit(EXIT_USAGE);
468
+ }
469
+ query.releaseFrom = `${releaseYear}-01-01`;
470
+ query.releaseTo = `${releaseYear}-12-31`;
471
+ }
472
+
473
+ function assertReviewBoundOptions(options) {
474
+ if (options["reviews-min"] != null && options["reviews-gt"] != null) {
475
+ writeJsonError({
476
+ code: "CLI_USAGE_ERROR",
477
+ message: "Use either --reviews-min or --reviews-gt, not both"
478
+ });
479
+ process.exit(EXIT_USAGE);
480
+ }
481
+ if (options["reviews-max"] != null && options["reviews-lt"] != null) {
482
+ writeJsonError({
483
+ code: "CLI_USAGE_ERROR",
484
+ message: "Use either --reviews-max or --reviews-lt, not both"
485
+ });
486
+ process.exit(EXIT_USAGE);
487
+ }
488
+ }
489
+
407
490
  function buildShowcaseSearchQuery(options) {
408
491
  return pickQuery(options, {
409
492
  query: "q",