@witchpot/gmc 0.1.3 → 0.1.5
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 +16 -0
- package/README.md +3 -0
- package/package.json +2 -1
- package/src/args.js +1 -1
- package/src/constants.js +2 -1
- package/src/help.js +4 -0
- package/src/requests.js +26 -0
- package/src/requests.test.js +104 -0
- package/src/workflows.js +11 -0
- package/src/workflows.test.js +80 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.5 - 2026-06-12
|
|
6
|
+
|
|
7
|
+
- Added `gmc games success-report` for the per-game Success Factor Analysis
|
|
8
|
+
report (executive summary plus markdown body; plan-gated like the API).
|
|
9
|
+
- Added `--coverage <basic|partial|full[,...]>` to games search, count,
|
|
10
|
+
aggregate, benchmark, resolve, review patterns, and cohort workflows so
|
|
11
|
+
agents can scope cohorts to researched titles before spending quota.
|
|
12
|
+
- Added cohort `skippedReasons` tallies to review-pattern and cohort evidence
|
|
13
|
+
summaries so "no data collected" is distinguishable from "no findings".
|
|
14
|
+
- Added a `node --test` unit harness for CLI request planning (`npm test`).
|
|
15
|
+
|
|
16
|
+
## 0.1.4 - 2026-06-01
|
|
17
|
+
|
|
18
|
+
- Reissued the beta CLI package after the count and aggregate API server
|
|
19
|
+
implementation was committed for deployment alignment.
|
|
20
|
+
|
|
5
21
|
## 0.1.3 - 2026-06-01
|
|
6
22
|
|
|
7
23
|
- Added `gmc games count` for fast Steam catalog counts with bounded filters.
|
package/README.md
CHANGED
|
@@ -76,9 +76,12 @@ when using output in agent answers.
|
|
|
76
76
|
- `gmc auth status`
|
|
77
77
|
- `gmc schema`
|
|
78
78
|
- `gmc games search`
|
|
79
|
+
- `gmc games count`
|
|
80
|
+
- `gmc games aggregate`
|
|
79
81
|
- `gmc games resolve`
|
|
80
82
|
- `gmc games detail`
|
|
81
83
|
- `gmc games analysis`
|
|
84
|
+
- `gmc games success-report`
|
|
82
85
|
- `gmc campaigns signals`
|
|
83
86
|
- `gmc games topics`
|
|
84
87
|
- `gmc games compare`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchpot/gmc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tools for Game Market Copilot market research.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
+
"test": "node --test src/*.test.js",
|
|
39
40
|
"release:check": "node scripts/check-release-readiness.mjs && node scripts/smoke-pack.mjs",
|
|
40
41
|
"prepublishOnly": "npm run release:check",
|
|
41
42
|
"smoke:beta": "node scripts/smoke-beta.mjs",
|
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", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics"].includes(token);
|
|
74
|
+
return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "success-report"].includes(token);
|
|
75
75
|
}
|
|
76
76
|
if (command[0] === "reviews") {
|
|
77
77
|
return token === "patterns";
|
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.5";
|
|
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");
|
|
@@ -25,6 +25,7 @@ export const SUPPORTED_BETA_COMMANDS = [
|
|
|
25
25
|
"gmc games resolve --source steam --query <title> --json",
|
|
26
26
|
"gmc games detail --source steam <externalId> --json",
|
|
27
27
|
"gmc games analysis --source steam <externalId> --sections reviews,marketing --json",
|
|
28
|
+
"gmc games success-report --source steam <externalId> --json",
|
|
28
29
|
"gmc campaigns signals --source steam <externalId> --json",
|
|
29
30
|
"gmc games topics --source steam <externalId> --json",
|
|
30
31
|
"gmc games compare --source steam <externalId> <externalId...> --focus market --json",
|
package/src/help.js
CHANGED
|
@@ -31,6 +31,7 @@ Usage:
|
|
|
31
31
|
gmc games detail --source steam <externalId> --json
|
|
32
32
|
gmc games detail --source steam <externalId> --fields title,snapshot.rating,snapshot.reviews --json
|
|
33
33
|
gmc games analysis --source steam <externalId> --sections reviews,marketing --json
|
|
34
|
+
gmc games success-report --source steam <externalId> --json
|
|
34
35
|
gmc campaigns signals --source steam <externalId> --json
|
|
35
36
|
gmc games compare --source steam <externalId> <externalId...> --focus market --json
|
|
36
37
|
gmc games topics --source steam <externalId> --json
|
|
@@ -81,6 +82,9 @@ Filters:
|
|
|
81
82
|
--rating-min <number> Inclusive rating lower bound, 0-100.
|
|
82
83
|
--price-min <number> Inclusive price lower bound.
|
|
83
84
|
--price-max <number> Inclusive price upper bound.
|
|
85
|
+
--coverage <list> Research coverage levels: basic, partial, full.
|
|
86
|
+
Use --coverage full,partial to keep only games
|
|
87
|
+
with collected analysis data.
|
|
84
88
|
|
|
85
89
|
Sorting:
|
|
86
90
|
--sort <key> reviews_desc, reviews_asc, rating_desc,
|
package/src/requests.js
CHANGED
|
@@ -129,6 +129,18 @@ export function buildRequest(command, options, positional) {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
if (matches(command, ["games", "success-report"])) {
|
|
133
|
+
assertSource(options.source);
|
|
134
|
+
const externalId = positional[0];
|
|
135
|
+
assertExternalId(externalId, "gmc games success-report --source steam <externalId> --json");
|
|
136
|
+
return {
|
|
137
|
+
path: `/api/v1/steam/games/${encodeURIComponent(externalId)}/success-report`,
|
|
138
|
+
query: pickQuery(options, {
|
|
139
|
+
locale: "locale"
|
|
140
|
+
})
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
if (matches(command, ["campaigns", "signals"])) {
|
|
133
145
|
assertSource(options.source);
|
|
134
146
|
const externalId = positional[0];
|
|
@@ -426,12 +438,14 @@ function buildGameCountQuery(options) {
|
|
|
426
438
|
"rating-min": "ratingMin",
|
|
427
439
|
"price-min": "priceMin",
|
|
428
440
|
"price-max": "priceMax",
|
|
441
|
+
coverage: "coverage",
|
|
429
442
|
locale: "locale"
|
|
430
443
|
});
|
|
431
444
|
|
|
432
445
|
applyReleaseYearAlias(query, options);
|
|
433
446
|
assertReviewBoundOptions(options);
|
|
434
447
|
if (query.tags) query.tags = normalizeTagSlugCsv(query.tags);
|
|
448
|
+
if (query.coverage) query.coverage = assertCoverageCsv(query.coverage);
|
|
435
449
|
return query;
|
|
436
450
|
}
|
|
437
451
|
|
|
@@ -588,6 +602,18 @@ function assertBoundedPositiveIntegerOption(value, optionName, maxValue) {
|
|
|
588
602
|
return number;
|
|
589
603
|
}
|
|
590
604
|
|
|
605
|
+
function assertCoverageCsv(value) {
|
|
606
|
+
const levels = parseCsvString(value).map((level) => level.toLowerCase());
|
|
607
|
+
if (levels.length === 0 || levels.some((level) => !["basic", "partial", "full"].includes(level))) {
|
|
608
|
+
writeJsonError({
|
|
609
|
+
code: "CLI_USAGE_ERROR",
|
|
610
|
+
message: "--coverage accepts a comma-separated subset of: basic, partial, full"
|
|
611
|
+
});
|
|
612
|
+
process.exit(EXIT_USAGE);
|
|
613
|
+
}
|
|
614
|
+
return levels.join(",");
|
|
615
|
+
}
|
|
616
|
+
|
|
591
617
|
function assertReviewPatternSentiment(value) {
|
|
592
618
|
const sentiment = String(value);
|
|
593
619
|
if (!["positive", "negative", "mixed"].includes(sentiment)) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { parseArgs } from "./args.js";
|
|
5
|
+
import { buildRequest } from "./requests.js";
|
|
6
|
+
|
|
7
|
+
function buildRequestFromArgv(argv) {
|
|
8
|
+
const { command, options, positional } = parseArgs(argv);
|
|
9
|
+
return buildRequest(command, options, positional);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test("games success-report builds the success-report request plan", () => {
|
|
13
|
+
const request = buildRequestFromArgv([
|
|
14
|
+
"games",
|
|
15
|
+
"success-report",
|
|
16
|
+
"--source",
|
|
17
|
+
"steam",
|
|
18
|
+
"1062090",
|
|
19
|
+
"--json"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
assert.equal(request.path, "/api/v1/steam/games/1062090/success-report");
|
|
23
|
+
assert.deepEqual(request.query, {});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("games success-report forwards locale", () => {
|
|
27
|
+
const request = buildRequestFromArgv([
|
|
28
|
+
"games",
|
|
29
|
+
"success-report",
|
|
30
|
+
"--source",
|
|
31
|
+
"steam",
|
|
32
|
+
"1062090",
|
|
33
|
+
"--locale",
|
|
34
|
+
"ja",
|
|
35
|
+
"--json"
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
assert.equal(request.path, "/api/v1/steam/games/1062090/success-report");
|
|
39
|
+
assert.deepEqual(request.query, { locale: "ja" });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("games detail request plan stays unchanged", () => {
|
|
43
|
+
const request = buildRequestFromArgv([
|
|
44
|
+
"games",
|
|
45
|
+
"detail",
|
|
46
|
+
"--source",
|
|
47
|
+
"steam",
|
|
48
|
+
"1062090",
|
|
49
|
+
"--json"
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
assert.equal(request.path, "/api/v1/steam/games/1062090");
|
|
53
|
+
assert.deepEqual(request.query, {});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("games search forwards coverage as a filter", () => {
|
|
57
|
+
const request = buildRequestFromArgv([
|
|
58
|
+
"games",
|
|
59
|
+
"search",
|
|
60
|
+
"--source",
|
|
61
|
+
"steam",
|
|
62
|
+
"--coverage",
|
|
63
|
+
"full,partial",
|
|
64
|
+
"--reviews-min",
|
|
65
|
+
"1000",
|
|
66
|
+
"--json"
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
assert.equal(request.path, "/api/v1/steam/games");
|
|
70
|
+
assert.equal(request.query.coverage, "full,partial");
|
|
71
|
+
assert.equal(request.query.reviewsMin, "1000");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("games count forwards coverage for cohort pre-estimation", () => {
|
|
75
|
+
const request = buildRequestFromArgv([
|
|
76
|
+
"games",
|
|
77
|
+
"count",
|
|
78
|
+
"--source",
|
|
79
|
+
"steam",
|
|
80
|
+
"--coverage",
|
|
81
|
+
"full",
|
|
82
|
+
"--json"
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
assert.equal(request.path, "/api/v1/steam/games/count");
|
|
86
|
+
assert.equal(request.query.coverage, "full");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("reviews patterns forwards coverage into the cohort search request", () => {
|
|
90
|
+
const request = buildRequestFromArgv([
|
|
91
|
+
"reviews",
|
|
92
|
+
"patterns",
|
|
93
|
+
"--source",
|
|
94
|
+
"steam",
|
|
95
|
+
"--tags",
|
|
96
|
+
"roguelike",
|
|
97
|
+
"--coverage",
|
|
98
|
+
"full,partial",
|
|
99
|
+
"--json"
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
assert.equal(request.kind, "reviewPatterns");
|
|
103
|
+
assert.equal(request.searchRequest.query.coverage, "full,partial");
|
|
104
|
+
});
|
package/src/workflows.js
CHANGED
|
@@ -437,6 +437,15 @@ export function composeBenchmarkPayload(request, search, options = {}) {
|
|
|
437
437
|
};
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
function tallySkippedReasons(skipped) {
|
|
441
|
+
const reasons = {};
|
|
442
|
+
for (const entry of skipped) {
|
|
443
|
+
const reason = entry?.reason ?? "unknown";
|
|
444
|
+
reasons[reason] = (reasons[reason] ?? 0) + 1;
|
|
445
|
+
}
|
|
446
|
+
return reasons;
|
|
447
|
+
}
|
|
448
|
+
|
|
440
449
|
export function composeReviewPatternsPayload(request, result, options = {}) {
|
|
441
450
|
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
442
451
|
const requestId = options.requestId ?? createCliRequestId();
|
|
@@ -495,6 +504,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
|
|
|
495
504
|
returnedGames: games.length,
|
|
496
505
|
usableGames: usableGames.length,
|
|
497
506
|
skippedGames: skipped.length,
|
|
507
|
+
skippedReasons: tallySkippedReasons(skipped),
|
|
498
508
|
totalMatches: result.search?.pagination?.total,
|
|
499
509
|
included: usableGames.map((game) => ({
|
|
500
510
|
source: request.source,
|
|
@@ -687,6 +697,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
|
|
|
687
697
|
returnedGames: games.length,
|
|
688
698
|
usableGames: included.length,
|
|
689
699
|
skippedGames: skipped.length,
|
|
700
|
+
skippedReasons: tallySkippedReasons(skipped),
|
|
690
701
|
totalMatches: result.search?.pagination?.total,
|
|
691
702
|
included: included.map((entry) => ({
|
|
692
703
|
source: request.source,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { composeCohortEvidencePayload } from "./workflows.js";
|
|
5
|
+
|
|
6
|
+
test("cohort evidence summarizes skipped games by reason", () => {
|
|
7
|
+
const request = {
|
|
8
|
+
source: "steam",
|
|
9
|
+
workflow: "cohortTrends",
|
|
10
|
+
evidenceType: "trends",
|
|
11
|
+
sections: [],
|
|
12
|
+
query: { sort: "reviews_desc", limit: 20 }
|
|
13
|
+
};
|
|
14
|
+
const result = {
|
|
15
|
+
search: {
|
|
16
|
+
data: [
|
|
17
|
+
{ appid: 1, title: "Usable", reviews: 100 },
|
|
18
|
+
{ appid: 2, title: "Failed A" },
|
|
19
|
+
{ appid: 3, title: "Failed B" },
|
|
20
|
+
{ appid: 4, title: "No Id" }
|
|
21
|
+
],
|
|
22
|
+
pagination: { limit: 20, offset: 0, total: 4, nextOffset: null }
|
|
23
|
+
},
|
|
24
|
+
analysisResults: [
|
|
25
|
+
{ game: { appid: 1, title: "Usable", reviews: 100 }, externalId: "1" },
|
|
26
|
+
{
|
|
27
|
+
game: { appid: 2, title: "Failed A" },
|
|
28
|
+
externalId: "2",
|
|
29
|
+
skippedReason: "analysis_request_failed",
|
|
30
|
+
error: { code: "UPSTREAM_UNAVAILABLE" }
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
game: { appid: 3, title: "Failed B" },
|
|
34
|
+
externalId: "3",
|
|
35
|
+
skippedReason: "analysis_request_failed",
|
|
36
|
+
error: { code: "UPSTREAM_UNAVAILABLE" }
|
|
37
|
+
},
|
|
38
|
+
{ game: { appid: 4, title: "No Id" }, skippedReason: "missing_external_id" }
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const payload = composeCohortEvidencePayload(request, result, {
|
|
43
|
+
generatedAt: "2026-06-12T00:00:00.000Z",
|
|
44
|
+
requestId: "req_test"
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(payload.data.cohort.returnedGames, 4);
|
|
48
|
+
assert.equal(payload.data.cohort.usableGames, 1);
|
|
49
|
+
assert.equal(payload.data.cohort.skippedGames, 3);
|
|
50
|
+
assert.deepEqual(payload.data.cohort.skippedReasons, {
|
|
51
|
+
analysis_request_failed: 2,
|
|
52
|
+
missing_external_id: 1
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("cohort evidence reports an empty reason tally when nothing is skipped", () => {
|
|
57
|
+
const request = {
|
|
58
|
+
source: "steam",
|
|
59
|
+
workflow: "cohortTrends",
|
|
60
|
+
evidenceType: "trends",
|
|
61
|
+
sections: [],
|
|
62
|
+
query: {}
|
|
63
|
+
};
|
|
64
|
+
const result = {
|
|
65
|
+
search: {
|
|
66
|
+
data: [{ appid: 1, title: "Usable", reviews: 100 }],
|
|
67
|
+
pagination: { limit: 20, offset: 0, total: 1, nextOffset: null }
|
|
68
|
+
},
|
|
69
|
+
analysisResults: [
|
|
70
|
+
{ game: { appid: 1, title: "Usable", reviews: 100 }, externalId: "1" }
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const payload = composeCohortEvidencePayload(request, result, {
|
|
75
|
+
generatedAt: "2026-06-12T00:00:00.000Z",
|
|
76
|
+
requestId: "req_test"
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(payload.data.cohort.skippedReasons, {});
|
|
80
|
+
});
|