@witchpot/gmc 0.1.1 → 0.1.3

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,7 +2,21 @@
2
2
 
3
3
  All notable changes to the Game Market Copilot CLI will be recorded here.
4
4
 
5
- ## 0.1.1 - Unreleased
5
+ ## 0.1.3 - 2026-06-01
6
+
7
+ - Added `gmc games count` for fast Steam catalog counts with bounded filters.
8
+ - Added `gmc games aggregate` for count, average, median, percentile, and bucket
9
+ summaries over Steam catalog cohorts.
10
+ - Added bucket threshold forwarding for price, review, and rating distribution
11
+ workflows.
12
+ - Fixed the npm executable entrypoint so `gmc` runs correctly through npm,
13
+ Volta, and other package-manager shims.
14
+ - Added packed-tarball smoke validation for actual CLI stdout to catch silent
15
+ executable startup regressions.
16
+ - Improved browser-login token exchange diagnostics for non-JSON Access
17
+ responses and accepted direct or nested credential response envelopes.
18
+
19
+ ## 0.1.1 - 2026-06-01
6
20
 
7
21
  - Updated npm package description and README introduction for public beta users.
8
22
 
package/README.md CHANGED
@@ -6,25 +6,21 @@ showcases.
6
6
 
7
7
  ## Install
8
8
 
9
- The CLI package is published to npm with a public package and beta dist-tag. If
10
- the beta package has not been published in the current environment yet, run from
11
- this repository:
9
+ Install the beta CLI:
12
10
 
13
11
  ```bash
14
- npm run gmc -- --help
12
+ npm install -g @witchpot/gmc@beta
13
+ gmc --version
15
14
  ```
16
15
 
17
- The package name for beta distribution is `@witchpot/gmc` with the
18
- `gmc` binary. Before publishing a beta build, run:
16
+ Or run it without a global install:
19
17
 
20
18
  ```bash
21
- npm run gmc:release:check
22
- npm run gmc:publish:beta:dry-run
23
- npm run gmc:publish:beta
19
+ npx @witchpot/gmc@beta --version
24
20
  ```
25
21
 
26
- Closed-beta setup details live in
27
- [`docs/closed-beta-onboarding.md`](../../docs/closed-beta-onboarding.md).
22
+ A fuller user guide lives in
23
+ [`docs/gmc-cli-user-guide.md`](../../docs/gmc-cli-user-guide.md).
28
24
 
29
25
  Closed-beta access is controlled by GMC API credentials, not npm package
30
26
  visibility. Installing the public package does not grant access to paid or beta
package/bin/gmc.mjs CHANGED
@@ -1,50 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { isCliEntryPoint, runCliEntryPoint } from "../src/main.js";
3
+ import { runCliEntryPoint } from "../src/main.js";
4
4
 
5
- export { BOOLEAN_OPTIONS, matches, parseArgs } from "../src/args.js";
6
- export {
7
- buildLoginUrl,
8
- exchangeAuthorizationCode,
9
- extractCredentialFromTokenPayload,
10
- runBrowserLogin
11
- } from "../src/browser-login.js";
12
- export { handleLocalCommand, loadConfig, parseBaseUrl } from "../src/config.js";
13
- export {
14
- CLI_BETA_PACKAGE_NAME,
15
- CLI_PACKAGE_NAME,
16
- CLI_VERSION,
17
- PROVISIONAL_BETA_COMMANDS,
18
- SUPPORTED_BETA_COMMANDS
19
- } from "../src/constants.js";
20
- export { writeJsonError } from "../src/output.js";
21
- export { applyFieldsProjection, applyOutputProjection } from "../src/projection.js";
22
- export {
23
- buildRequest,
24
- buildRequestHeaders,
25
- createRequestUrl,
26
- filterShowcasesByDeadline
27
- } from "../src/requests.js";
28
- export {
29
- buildHttpApiError,
30
- buildNonJsonApiError,
31
- composeBenchmarkPayload,
32
- composeCampaignSignalsPayload,
33
- composeCohortEvidencePayload,
34
- composeComparePayload,
35
- composeReviewPatternsPayload,
36
- composeResolvePayload,
37
- composeShowcaseFitPayload,
38
- executeRequestPlan
39
- } from "../src/workflows.js";
40
- export {
41
- buildCliAuthStatus,
42
- enrichAuthStatusOutput,
43
- isCliEntryPoint,
44
- main,
45
- runCliEntryPoint
46
- } from "../src/main.js";
47
-
48
- if (isCliEntryPoint(import.meta.url)) {
49
- runCliEntryPoint();
50
- }
5
+ runCliEntryPoint();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchpot/gmc",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Command line tools for Game Market Copilot market research.",
6
6
  "license": "UNLICENSED",
@@ -14,6 +14,11 @@
14
14
  "url": "https://gamemarketcopilot.com"
15
15
  },
16
16
  "type": "module",
17
+ "main": "./src/index.js",
18
+ "exports": {
19
+ ".": "./src/index.js",
20
+ "./package.json": "./package.json"
21
+ },
17
22
  "bin": {
18
23
  "gmc": "bin/gmc.mjs"
19
24
  },
@@ -9,6 +9,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
9
9
 
10
10
  const requiredFiles = [
11
11
  "bin/gmc.mjs",
12
+ "src/index.js",
12
13
  "src/main.js",
13
14
  "src/requests.js",
14
15
  "src/workflows.js",
@@ -26,6 +27,7 @@ const forbiddenPatterns = [
26
27
 
27
28
  const sourceFiles = [
28
29
  "bin/gmc.mjs",
30
+ "src/index.js",
29
31
  "src/args.js",
30
32
  "src/browser-login.js",
31
33
  "src/config.js",
@@ -56,6 +58,14 @@ if (packageJson.bin?.gmc !== "bin/gmc.mjs") {
56
58
  failures.push("bin.gmc must point to bin/gmc.mjs.");
57
59
  }
58
60
 
61
+ if (packageJson.main !== "./src/index.js") {
62
+ failures.push("main must point to ./src/index.js.");
63
+ }
64
+
65
+ if (packageJson.exports?.["."] !== "./src/index.js") {
66
+ failures.push("exports['.'] must point to ./src/index.js.");
67
+ }
68
+
59
69
  for (const required of requiredFileGlobs) {
60
70
  if (!packageJson.files?.includes(required)) {
61
71
  failures.push(`package.json files must include ${required}.`);
@@ -48,8 +48,22 @@ try {
48
48
  const binName = process.platform === "win32" ? "gmc.cmd" : "gmc";
49
49
  const gmcBin = path.join(workDir, "node_modules", ".bin", binName);
50
50
  const configPath = path.join(projectDir, "config.json");
51
- run(gmcBin, ["--help"], { env: { GMC_CONFIG_PATH: configPath } });
52
- run(gmcBin, ["config", "get", "--json"], { env: { GMC_CONFIG_PATH: configPath } });
51
+ const helpResult = run(gmcBin, ["--help"], {
52
+ env: { GMC_CONFIG_PATH: configPath },
53
+ capture: true
54
+ });
55
+ if (!helpResult.stdout.includes("Game Market Copilot CLI") || !helpResult.stdout.includes("gmc --version")) {
56
+ throw new Error("Packed gmc --help did not print the expected help text");
57
+ }
58
+
59
+ const configResult = run(gmcBin, ["config", "get", "--json"], {
60
+ env: { GMC_CONFIG_PATH: configPath },
61
+ capture: true
62
+ });
63
+ const configPayload = JSON.parse(configResult.stdout);
64
+ if (configPayload?.data?.configPath !== configPath || configPayload?.data?.authMode !== "anonymous") {
65
+ throw new Error("Packed gmc config get --json did not print the expected config payload");
66
+ }
53
67
  } finally {
54
68
  rmSync(workDir, { recursive: true, force: true });
55
69
  }
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.1";
11
+ export const CLI_VERSION = "0.1.3";
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 ADDED
@@ -0,0 +1,42 @@
1
+ export { BOOLEAN_OPTIONS, matches, parseArgs } from "./args.js";
2
+ export {
3
+ buildLoginUrl,
4
+ exchangeAuthorizationCode,
5
+ extractCredentialFromTokenPayload,
6
+ isJsonContentType,
7
+ runBrowserLogin
8
+ } from "./browser-login.js";
9
+ export { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
10
+ export {
11
+ CLI_BETA_PACKAGE_NAME,
12
+ CLI_PACKAGE_NAME,
13
+ CLI_VERSION,
14
+ PROVISIONAL_BETA_COMMANDS,
15
+ SUPPORTED_BETA_COMMANDS
16
+ } from "./constants.js";
17
+ export { writeJsonError } from "./output.js";
18
+ export { applyFieldsProjection, applyOutputProjection } from "./projection.js";
19
+ export {
20
+ buildRequest,
21
+ buildRequestHeaders,
22
+ createRequestUrl,
23
+ filterShowcasesByDeadline
24
+ } from "./requests.js";
25
+ export {
26
+ buildCliAuthStatus,
27
+ enrichAuthStatusOutput,
28
+ main,
29
+ runCliEntryPoint
30
+ } from "./main.js";
31
+ export {
32
+ buildHttpApiError,
33
+ buildNonJsonApiError,
34
+ composeBenchmarkPayload,
35
+ composeCampaignSignalsPayload,
36
+ composeCohortEvidencePayload,
37
+ composeComparePayload,
38
+ composeReviewPatternsPayload,
39
+ composeResolvePayload,
40
+ composeShowcaseFitPayload,
41
+ executeRequestPlan
42
+ } from "./workflows.js";
package/src/main.js CHANGED
@@ -1,4 +1,3 @@
1
- import { pathToFileURL } from "node:url";
2
1
  import {
3
2
  CLI_PACKAGE_NAME,
4
3
  CLI_VERSION,
@@ -9,7 +8,7 @@ import {
9
8
  } from "./constants.js";
10
9
  import { matches, parseArgs } from "./args.js";
11
10
  import { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
12
- import { printHelp } from "./help.js";
11
+ import { printGamesSearchHelp, printHelp } from "./help.js";
13
12
  import { writeJsonError } from "./output.js";
14
13
  import { applyOutputProjection } from "./projection.js";
15
14
  import { buildRequest } from "./requests.js";
@@ -32,7 +31,16 @@ export async function main(argv = process.argv.slice(2)) {
32
31
  process.exit(0);
33
32
  }
34
33
 
35
- 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) {
36
44
  printHelp();
37
45
  process.exit(0);
38
46
  }
@@ -89,10 +97,6 @@ export function runCliEntryPoint() {
89
97
  });
90
98
  }
91
99
 
92
- export function isCliEntryPoint(importMetaUrl, argvEntry = process.argv[1]) {
93
- return Boolean(argvEntry && importMetaUrl === pathToFileURL(argvEntry).href);
94
- }
95
-
96
100
  export function enrichAuthStatusOutput(payload, config) {
97
101
  if (!payload || typeof payload !== "object" || !payload.data || typeof payload.data !== "object") {
98
102
  return payload;
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",