@witchpot/gmc 0.1.2 → 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 +8 -1
- package/package.json +1 -1
- package/src/args.js +1 -1
- package/src/browser-login.js +8 -2
- package/src/constants.js +3 -1
- package/src/help.js +50 -0
- package/src/index.js +1 -0
- package/src/main.js +11 -2
- package/src/requests.js +86 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
-
## 0.1.
|
|
5
|
+
## 0.1.3 - 2026-06-01
|
|
6
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.
|
|
7
12
|
- Fixed the npm executable entrypoint so `gmc` runs correctly through npm,
|
|
8
13
|
Volta, and other package-manager shims.
|
|
9
14
|
- Added packed-tarball smoke validation for actual CLI stdout to catch silent
|
|
10
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.
|
|
11
18
|
|
|
12
19
|
## 0.1.1 - 2026-06-01
|
|
13
20
|
|
package/package.json
CHANGED
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";
|
package/src/browser-login.js
CHANGED
|
@@ -88,10 +88,11 @@ export async function exchangeAuthorizationCode({
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
const contentType = response.headers.get("content-type") ?? "";
|
|
91
|
-
const
|
|
91
|
+
const isJson = isJsonContentType(contentType);
|
|
92
|
+
const payload = isJson
|
|
92
93
|
? await response.json().catch(() => null)
|
|
93
94
|
: null;
|
|
94
|
-
if (!
|
|
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.
|
|
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
CHANGED
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
|
|
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",
|