@witchpot/gmc 0.1.8 → 0.2.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 +17 -0
- package/README.md +17 -12
- package/package.json +3 -1
- package/scripts/smoke-beta.mjs +35 -9
- package/skill/gmc-analysis/SKILL.md +5 -0
- package/skill/gmc-analysis/references/pitfalls.md +3 -1
- package/skill/gmc-analysis/references/quota.md +3 -0
- package/skill/gmc-analysis/references/recipes.md +3 -1
- package/src/args.js +5 -1
- package/src/config.js +5 -5
- package/src/constants.js +4 -1
- package/src/help.js +9 -2
- package/src/main.js +12 -0
- package/src/requests.js +204 -10
- package/src/requests.test.js +101 -6
- package/src/skill.js +2 -1
- package/src/telemetry.js +341 -0
- package/src/telemetry.test.js +121 -0
- package/src/workflows.js +133 -36
- package/src/workflows.test.js +83 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.2.0 - 2026-07-04
|
|
8
|
+
|
|
9
|
+
- Added workspace game list commands for creating, listing, resolving, and
|
|
10
|
+
mutating saved cohorts from the CLI.
|
|
11
|
+
- Added saved game lists as a cohort source for agent workflows and evidence
|
|
12
|
+
packs, including manual-list item notes.
|
|
13
|
+
- Updated packaged schema/help output to include the new list routes and
|
|
14
|
+
commands.
|
|
15
|
+
|
|
16
|
+
## 0.1.9 - 2026-07-03
|
|
17
|
+
|
|
18
|
+
- Clarified packaged Agent Skill docs so Claude Code uses `gmc skill install`
|
|
19
|
+
and Codex/AGENTS.md-based agents use
|
|
20
|
+
`gmc skill print --format agents-md >> AGENTS.md`.
|
|
21
|
+
|
|
5
22
|
## 0.1.8 - 2026-06-12
|
|
6
23
|
|
|
7
24
|
- Updated the bundled `gmc-analysis` skill to v0.2.0 with the learnings from
|
package/README.md
CHANGED
|
@@ -6,17 +6,17 @@ showcases.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
Install the
|
|
9
|
+
Install the CLI:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install -g @witchpot/gmc
|
|
12
|
+
npm install -g @witchpot/gmc
|
|
13
13
|
gmc --version
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Or run it without a global install:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npx @witchpot/gmc
|
|
19
|
+
npx @witchpot/gmc --version
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The package installs three equivalent binaries: `gmc`, `gmcp`, and
|
|
@@ -64,8 +64,10 @@ a non-secret prefix.
|
|
|
64
64
|
## Agent Skill
|
|
65
65
|
|
|
66
66
|
The package bundles the `gmc-analysis` Agent Skill — analysis recipes, pivot
|
|
67
|
-
heuristics, quota etiquette, and honesty rules for coding agents.
|
|
68
|
-
|
|
67
|
+
heuristics, quota etiquette, and honesty rules for coding agents. The core
|
|
68
|
+
analysis guidance is shared, but the registration step depends on the agent.
|
|
69
|
+
|
|
70
|
+
For Claude Code, install the packaged skill directory:
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
73
|
gmc skill install # writes .claude/skills/gmc-analysis (project)
|
|
@@ -73,13 +75,16 @@ gmc skill install --global # or ~/.claude/skills/gmc-analysis
|
|
|
73
75
|
gmc skill status --json # packaged vs installed versions
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
For Codex and other AGENTS.md-based agents, append the self-contained
|
|
77
|
-
|
|
78
|
+
For Codex and other AGENTS.md-based agents, append the self-contained edition
|
|
79
|
+
(references inlined) to the repo brief the agent reads:
|
|
78
80
|
|
|
79
81
|
```bash
|
|
80
82
|
gmc skill print --format agents-md >> AGENTS.md
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
If `AGENTS.md` already contains a generated `gmc-analysis` section, update that
|
|
86
|
+
section instead of appending a duplicate.
|
|
87
|
+
|
|
83
88
|
Re-run install/print after `npm update` to pick up newer skill versions.
|
|
84
89
|
|
|
85
90
|
## First Queries
|
|
@@ -87,11 +92,11 @@ Re-run install/print after `npm update` to pick up newer skill versions.
|
|
|
87
92
|
```bash
|
|
88
93
|
gmc schema --json
|
|
89
94
|
gmc update --json
|
|
90
|
-
gmc games
|
|
91
|
-
gmc games
|
|
92
|
-
gmc
|
|
93
|
-
gmc
|
|
94
|
-
gmc showcases
|
|
95
|
+
gmc games search --source steam --release-year 2025 --reviews-min 1000 --fields title,appid,rating,reviews --json
|
|
96
|
+
gmc games aggregate --source steam --release-year 2025 --metric count,median_reviews --group-by release_year --reviews-min 1000 --json
|
|
97
|
+
gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min 1000 --preset summary --json
|
|
98
|
+
gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --sample-size 15 --preset summary --json
|
|
99
|
+
gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
Preserve `meta.requestId`, `meta.entitlements`, `warnings`, and `pagination`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchpot/gmc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tools for Game Market Copilot market research.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"smoke:beta": "node scripts/smoke-beta.mjs",
|
|
46
46
|
"pack:check": "node scripts/smoke-pack.mjs --dry-run",
|
|
47
47
|
"smoke:pack": "node scripts/smoke-pack.mjs",
|
|
48
|
+
"publish:latest:dry-run": "npm publish --access public --dry-run --cache /tmp/gmc-npm-cache",
|
|
49
|
+
"publish:latest": "npm publish --access public --cache /tmp/gmc-npm-cache",
|
|
48
50
|
"publish:beta:dry-run": "npm publish --tag beta --access public --dry-run --cache /tmp/gmc-npm-cache",
|
|
49
51
|
"publish:beta": "npm publish --tag beta --access public --cache /tmp/gmc-npm-cache"
|
|
50
52
|
}
|
package/scripts/smoke-beta.mjs
CHANGED
|
@@ -33,14 +33,40 @@ const smokeCommands = [
|
|
|
33
33
|
validate: (payload) => requireSuccessEnvelope(payload, ["data.version", "data.cliCommands"])
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
name: "
|
|
37
|
-
args: [
|
|
38
|
-
|
|
36
|
+
name: "search recent reviewed games",
|
|
37
|
+
args: [
|
|
38
|
+
"games",
|
|
39
|
+
"search",
|
|
40
|
+
"--source",
|
|
41
|
+
"steam",
|
|
42
|
+
"--release-year",
|
|
43
|
+
"2025",
|
|
44
|
+
"--reviews-min",
|
|
45
|
+
"1000",
|
|
46
|
+
"--limit",
|
|
47
|
+
"5",
|
|
48
|
+
"--json"
|
|
49
|
+
],
|
|
50
|
+
validate: (payload) => requireSuccessEnvelope(payload, ["data"])
|
|
39
51
|
},
|
|
40
52
|
{
|
|
41
|
-
name: "
|
|
42
|
-
args: [
|
|
43
|
-
|
|
53
|
+
name: "aggregate recent reviewed games",
|
|
54
|
+
args: [
|
|
55
|
+
"games",
|
|
56
|
+
"aggregate",
|
|
57
|
+
"--source",
|
|
58
|
+
"steam",
|
|
59
|
+
"--release-year",
|
|
60
|
+
"2025",
|
|
61
|
+
"--metric",
|
|
62
|
+
"count,median_reviews",
|
|
63
|
+
"--group-by",
|
|
64
|
+
"release_year",
|
|
65
|
+
"--reviews-min",
|
|
66
|
+
"1000",
|
|
67
|
+
"--json"
|
|
68
|
+
],
|
|
69
|
+
validate: (payload) => requireSuccessEnvelope(payload, ["data"])
|
|
44
70
|
},
|
|
45
71
|
{
|
|
46
72
|
name: "benchmark city builder automation",
|
|
@@ -60,9 +86,9 @@ const smokeCommands = [
|
|
|
60
86
|
validate: (payload) => requireComposedEnvelope(payload, ["data.summary"])
|
|
61
87
|
},
|
|
62
88
|
{
|
|
63
|
-
name: "showcase
|
|
64
|
-
args: ["showcases", "
|
|
65
|
-
validate: (payload) => requireComposedEnvelope(payload, ["data.
|
|
89
|
+
name: "showcase patterns by tag",
|
|
90
|
+
args: ["showcases", "patterns", "--source", "steam", "--tags", "city_builder,automation", "--json"],
|
|
91
|
+
validate: (payload) => requireComposedEnvelope(payload, ["data.summary"])
|
|
66
92
|
}
|
|
67
93
|
];
|
|
68
94
|
|
|
@@ -53,6 +53,11 @@ For cohorts beyond one page, and for the full-cohort map-reduce procedure
|
|
|
53
53
|
(pagination, bounded concurrency, `--fields` projection, subagent chunking),
|
|
54
54
|
follow the recipes reference.
|
|
55
55
|
|
|
56
|
+
Search commands are optimized for row retrieval. Do not expect
|
|
57
|
+
`pagination.total` from `gmc games search` or CLI-composed evidence packs
|
|
58
|
+
unless you explicitly pass `--include-total`; for count-only questions use
|
|
59
|
+
`gmc games count` instead.
|
|
60
|
+
|
|
56
61
|
## Hard rules
|
|
57
62
|
|
|
58
63
|
- **Missing data is not absent sentiment.** Report `cohort.usableGames`,
|
|
@@ -69,4 +69,6 @@ exemplar rows and caveat bucket assignments, especially during sale seasons.
|
|
|
69
69
|
List endpoints cap at `limit=100`. For "how many" questions use
|
|
70
70
|
`gmc games count`; for distributions use `gmc games aggregate` — both are
|
|
71
71
|
single-request server aggregates. Reserve pagination loops for when you
|
|
72
|
-
actually need the rows (recipe R2).
|
|
72
|
+
actually need the rows (recipe R2). `gmc games search` is optimized for rows
|
|
73
|
+
and may omit `pagination.total`; pass `--include-total` only when a row page
|
|
74
|
+
also needs an exact count.
|
|
@@ -25,3 +25,6 @@ indicative — the response metadata is the source of truth.
|
|
|
25
25
|
midnight), and tell the user how much of the analysis completed.
|
|
26
26
|
5. **Prefer server aggregates** (`count`, `aggregate`) over row pagination —
|
|
27
27
|
they answer sizing questions in one request.
|
|
28
|
+
6. **Keep totals explicit.** Search commands prioritize fast row pages and
|
|
29
|
+
exact totals are opt-in (`--include-total`). Use `games count` for
|
|
30
|
+
denominators so row collection stays cheap.
|
|
@@ -36,7 +36,9 @@ Cost: ~(pages + N) requests. Check quota headroom first (see quota.md).
|
|
|
36
36
|
1. **Collect the cohort**: page `gmc games search <filters> --coverage
|
|
37
37
|
full,partial --sort reviews_desc --limit 100 --offset <0,100,...>
|
|
38
38
|
--fields appid,title,reviews --json` until `pagination.nextOffset` is
|
|
39
|
-
null.
|
|
39
|
+
null. Run `gmc games count` first when you need the denominator; add
|
|
40
|
+
`--include-total` to search only when the exact total must travel with the
|
|
41
|
+
row page.
|
|
40
42
|
2. **Fan out** `gmc games analysis --source steam <appid> --sections
|
|
41
43
|
reviews --json` per game with bounded concurrency (<= 8 parallel
|
|
42
44
|
processes). Write each response to a temp file; tolerate individual
|
package/src/args.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EXIT_USAGE } from "./constants.js";
|
|
2
2
|
import { writeJsonError } from "./output.js";
|
|
3
3
|
|
|
4
|
-
export const BOOLEAN_OPTIONS = new Set(["json", "help", "version", "no-open", "global"]);
|
|
4
|
+
export const BOOLEAN_OPTIONS = new Set(["json", "help", "version", "no-open", "global", "include-total"]);
|
|
5
5
|
|
|
6
6
|
export function parseArgs(args) {
|
|
7
7
|
const command = [];
|
|
@@ -56,6 +56,7 @@ function isCommandToken(command, token) {
|
|
|
56
56
|
"creators",
|
|
57
57
|
"cohort",
|
|
58
58
|
"showcases",
|
|
59
|
+
"lists",
|
|
59
60
|
"auth",
|
|
60
61
|
"login",
|
|
61
62
|
"logout",
|
|
@@ -95,6 +96,9 @@ function isCommandToken(command, token) {
|
|
|
95
96
|
if (command[0] === "showcases") {
|
|
96
97
|
return ["search", "fit", "patterns"].includes(token);
|
|
97
98
|
}
|
|
99
|
+
if (command[0] === "lists") {
|
|
100
|
+
return ["create", "show", "games", "add", "remove", "delete"].includes(token);
|
|
101
|
+
}
|
|
98
102
|
return false;
|
|
99
103
|
}
|
|
100
104
|
|
package/src/config.js
CHANGED
|
@@ -187,15 +187,15 @@ export async function handleLocalCommand(command, options, positional, config) {
|
|
|
187
187
|
status: "manual_update_required",
|
|
188
188
|
updateCheck: {
|
|
189
189
|
available: null,
|
|
190
|
-
reason: "The CLI does not query the npm registry yet. Use
|
|
190
|
+
reason: "The CLI does not query the npm registry yet. Use npm install or npx to run the current public package."
|
|
191
191
|
},
|
|
192
192
|
commands: {
|
|
193
|
-
npmGlobal: `npm install -g ${
|
|
194
|
-
npmExec: `npx ${
|
|
193
|
+
npmGlobal: `npm install -g ${CLI_PACKAGE_NAME}`,
|
|
194
|
+
npmExec: `npx ${CLI_PACKAGE_NAME} --version`
|
|
195
195
|
},
|
|
196
196
|
notes: [
|
|
197
|
-
"This command is intentionally non-mutating
|
|
198
|
-
"A future gmc update --apply command can run the package-manager update after
|
|
197
|
+
"This command is intentionally non-mutating.",
|
|
198
|
+
"A future gmc update --apply command can run the package-manager update after registry checks are implemented."
|
|
199
199
|
]
|
|
200
200
|
}
|
|
201
201
|
};
|
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.
|
|
11
|
+
export const CLI_VERSION = "0.2.0";
|
|
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");
|
|
@@ -33,6 +33,8 @@ export const SUPPORTED_BETA_COMMANDS = [
|
|
|
33
33
|
"gmc games topics --source steam <externalId> --json",
|
|
34
34
|
"gmc games compare --source steam <externalId> <externalId...> --focus market --json",
|
|
35
35
|
"gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
|
|
36
|
+
"gmc lists --json",
|
|
37
|
+
"gmc lists games <id> --json",
|
|
36
38
|
"gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
|
|
37
39
|
"gmc showcases search --query <text> --json",
|
|
38
40
|
"gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json"
|
|
@@ -47,6 +49,7 @@ export const PROVISIONAL_BETA_COMMANDS = [
|
|
|
47
49
|
"gmc cohort pricing --source steam --tags <tag_slug[,tag_slug...]> --json",
|
|
48
50
|
"gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json",
|
|
49
51
|
"gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json",
|
|
52
|
+
"gmc cohort brief --source steam --list <id> --json",
|
|
50
53
|
"gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json"
|
|
51
54
|
];
|
|
52
55
|
|
package/src/help.js
CHANGED
|
@@ -25,6 +25,10 @@ Usage:
|
|
|
25
25
|
gmc skill print --format agents-md
|
|
26
26
|
gmc skill status --json
|
|
27
27
|
gmc schema --json
|
|
28
|
+
gmc lists --json
|
|
29
|
+
gmc lists create --name <name> --kind manual --json
|
|
30
|
+
gmc lists games <id> --json
|
|
31
|
+
gmc lists add <id> --app-ids <appid[,appid...]> --json
|
|
28
32
|
gmc games search --source steam --query <text> --json
|
|
29
33
|
gmc games count --source steam --release-year <year> --reviews-gt <number> --json
|
|
30
34
|
gmc games aggregate --source steam --metric count,median_reviews --group-by release_year --json
|
|
@@ -47,6 +51,7 @@ Usage:
|
|
|
47
51
|
gmc cohort pricing --source steam --tags <tag_slug[,tag_slug...]> --json
|
|
48
52
|
gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json
|
|
49
53
|
gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json
|
|
54
|
+
gmc cohort brief --source steam --list <id> --json
|
|
50
55
|
gmc showcases search --query <text> --json
|
|
51
56
|
gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json
|
|
52
57
|
gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json
|
|
@@ -56,10 +61,10 @@ Binary aliases:
|
|
|
56
61
|
alias shadows gmc (Oh My Zsh's git plugin defines gmc as
|
|
57
62
|
'git merge --continue').
|
|
58
63
|
|
|
59
|
-
Supported
|
|
64
|
+
Supported commands:
|
|
60
65
|
${SUPPORTED_BETA_COMMANDS.map((command) => ` ${command}`).join("\n")}
|
|
61
66
|
|
|
62
|
-
Provisional
|
|
67
|
+
Provisional commands:
|
|
63
68
|
${PROVISIONAL_BETA_COMMANDS.map((command) => ` ${command}`).join("\n")}
|
|
64
69
|
|
|
65
70
|
Environment:
|
|
@@ -102,6 +107,8 @@ Sorting:
|
|
|
102
107
|
Pagination:
|
|
103
108
|
--limit <number> 1-100, default 20.
|
|
104
109
|
--offset <number> Offset for the next page.
|
|
110
|
+
--include-total Also fetch an exact total. Prefer games count
|
|
111
|
+
when the user only asks how many games match.
|
|
105
112
|
|
|
106
113
|
Output:
|
|
107
114
|
--fields <list> Optional comma-separated field list.
|
package/src/main.js
CHANGED
|
@@ -12,6 +12,7 @@ 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";
|
|
15
|
+
import { trackCliCommandExecuted } from "./telemetry.js";
|
|
15
16
|
import { executeRequestPlan } from "./workflows.js";
|
|
16
17
|
|
|
17
18
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -78,6 +79,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
78
79
|
process.exit(EXIT_CONFIG);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
const startedAt = Date.now();
|
|
81
83
|
const response = await executeRequestPlan(baseUrl, request, { apiKey });
|
|
82
84
|
const payload = matches(command, ["auth", "status"])
|
|
83
85
|
? enrichAuthStatusOutput(response, config)
|
|
@@ -89,6 +91,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
94
|
+
await trackCliCommandExecuted({
|
|
95
|
+
baseUrl,
|
|
96
|
+
apiKey,
|
|
97
|
+
command,
|
|
98
|
+
options,
|
|
99
|
+
positional,
|
|
100
|
+
request,
|
|
101
|
+
response: payload,
|
|
102
|
+
startedAt
|
|
103
|
+
});
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
export function runCliEntryPoint() {
|
package/src/requests.js
CHANGED
|
@@ -37,6 +37,80 @@ export function buildRequest(command, options, positional) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
if (matches(command, ["lists"])) {
|
|
41
|
+
return {
|
|
42
|
+
path: "/api/v1/lists",
|
|
43
|
+
query: {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (matches(command, ["lists", "create"])) {
|
|
48
|
+
const kind = assertListKind(options.kind ?? "manual");
|
|
49
|
+
return {
|
|
50
|
+
path: "/api/v1/lists",
|
|
51
|
+
method: "POST",
|
|
52
|
+
query: {},
|
|
53
|
+
body: {
|
|
54
|
+
name: assertRequiredOption(options.name, "--name"),
|
|
55
|
+
kind,
|
|
56
|
+
...(options.description ? { description: options.description } : {}),
|
|
57
|
+
...(kind === "filter" ? { filter: parseJsonOption(options.filter, "--filter") } : {}),
|
|
58
|
+
...(options.sort ? { sort: parseJsonOption(options.sort, "--sort") } : {})
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (matches(command, ["lists", "show"])) {
|
|
64
|
+
const id = assertListId(positional[0], "gmc lists show <id> --json");
|
|
65
|
+
return {
|
|
66
|
+
path: `/api/v1/lists/${encodeURIComponent(id)}`,
|
|
67
|
+
query: {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (matches(command, ["lists", "games"])) {
|
|
72
|
+
const id = assertListId(positional[0], "gmc lists games <id> --json");
|
|
73
|
+
return {
|
|
74
|
+
path: `/api/v1/lists/${encodeURIComponent(id)}/games`,
|
|
75
|
+
query: pickQuery(options, {
|
|
76
|
+
limit: "limit",
|
|
77
|
+
offset: "offset"
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (matches(command, ["lists", "add"])) {
|
|
83
|
+
const id = assertListId(positional[0], "gmc lists add <id> --app-ids <ids> --json");
|
|
84
|
+
return {
|
|
85
|
+
path: `/api/v1/lists/${encodeURIComponent(id)}/items`,
|
|
86
|
+
method: "POST",
|
|
87
|
+
query: {},
|
|
88
|
+
body: {
|
|
89
|
+
appids: parseAppIdsOption(options["app-ids"] ?? options.appids),
|
|
90
|
+
...(options.note ? { note: options.note } : {})
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (matches(command, ["lists", "remove"])) {
|
|
96
|
+
const id = assertListId(positional[0], "gmc lists remove <id> --app-ids <id> --json");
|
|
97
|
+
const [appid] = parseAppIdsOption(options["app-ids"] ?? options.appids);
|
|
98
|
+
return {
|
|
99
|
+
path: `/api/v1/lists/${encodeURIComponent(id)}/items/${encodeURIComponent(String(appid))}`,
|
|
100
|
+
method: "DELETE",
|
|
101
|
+
query: {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (matches(command, ["lists", "delete"])) {
|
|
106
|
+
const id = assertListId(positional[0], "gmc lists delete <id> --json");
|
|
107
|
+
return {
|
|
108
|
+
path: `/api/v1/lists/${encodeURIComponent(id)}`,
|
|
109
|
+
method: "DELETE",
|
|
110
|
+
query: {}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
40
114
|
if (matches(command, ["games", "search"])) {
|
|
41
115
|
assertSource(options.source);
|
|
42
116
|
return {
|
|
@@ -88,7 +162,7 @@ export function buildRequest(command, options, positional) {
|
|
|
88
162
|
const query = {
|
|
89
163
|
sort: "reviews_desc",
|
|
90
164
|
limit: sampling?.pageLimit ?? DEFAULT_BENCHMARK_LIMIT,
|
|
91
|
-
...buildGameSearchQuery(options)
|
|
165
|
+
...buildGameSearchQuery(options, { includeTotal: false })
|
|
92
166
|
};
|
|
93
167
|
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
94
168
|
return {
|
|
@@ -96,6 +170,7 @@ export function buildRequest(command, options, positional) {
|
|
|
96
170
|
source: "steam",
|
|
97
171
|
query,
|
|
98
172
|
sampling,
|
|
173
|
+
countRequest: buildOptionalGameCountRequest(options),
|
|
99
174
|
searchRequest: {
|
|
100
175
|
path: "/api/v1/steam/games",
|
|
101
176
|
query
|
|
@@ -198,11 +273,20 @@ export function buildRequest(command, options, positional) {
|
|
|
198
273
|
);
|
|
199
274
|
const sentiment = options.sentiment == null ? undefined : assertReviewPatternSentiment(options.sentiment);
|
|
200
275
|
const sampling = buildSamplingOptions(options, DEFAULT_REVIEW_PATTERNS_LIMIT);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
276
|
+
const listId = options.list == null ? undefined : assertListId(options.list, "--list <id>");
|
|
277
|
+
const query = listId
|
|
278
|
+
? {
|
|
279
|
+
list: listId,
|
|
280
|
+
limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
|
|
281
|
+
...pickQuery(options, {
|
|
282
|
+
offset: "offset"
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
: {
|
|
286
|
+
sort: "reviews_desc",
|
|
287
|
+
limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
|
|
288
|
+
...buildGameSearchQuery(options, { includeTotal: false })
|
|
289
|
+
};
|
|
206
290
|
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
207
291
|
|
|
208
292
|
return {
|
|
@@ -210,13 +294,14 @@ export function buildRequest(command, options, positional) {
|
|
|
210
294
|
source: "steam",
|
|
211
295
|
query,
|
|
212
296
|
sampling,
|
|
297
|
+
countRequest: listId ? undefined : buildOptionalGameCountRequest(options),
|
|
213
298
|
minGames,
|
|
214
299
|
maxTopicsPerGame,
|
|
215
300
|
sentiment,
|
|
216
301
|
topic: options.topic,
|
|
217
302
|
compareBy: isCompare ? String(options.segment ?? "price") : undefined,
|
|
218
303
|
searchRequest: {
|
|
219
|
-
path: "/api/v1/steam/games",
|
|
304
|
+
path: listId ? `/api/v1/lists/${encodeURIComponent(listId)}/games` : "/api/v1/steam/games",
|
|
220
305
|
query
|
|
221
306
|
},
|
|
222
307
|
topicQuery: {
|
|
@@ -358,10 +443,42 @@ export function buildRequest(command, options, positional) {
|
|
|
358
443
|
function buildCohortEvidenceRequest({ workflow, evidenceType, sections, options }) {
|
|
359
444
|
assertSource(options.source);
|
|
360
445
|
const sampling = buildSamplingOptions(options, DEFAULT_COHORT_WORKFLOW_LIMIT);
|
|
446
|
+
if (options.list != null) {
|
|
447
|
+
const listId = assertListId(options.list, "--list <id>");
|
|
448
|
+
const query = {
|
|
449
|
+
list: listId,
|
|
450
|
+
limit: sampling?.pageLimit ?? DEFAULT_COHORT_WORKFLOW_LIMIT,
|
|
451
|
+
...pickQuery(options, {
|
|
452
|
+
offset: "offset"
|
|
453
|
+
})
|
|
454
|
+
};
|
|
455
|
+
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
456
|
+
return {
|
|
457
|
+
kind: "cohortEvidence",
|
|
458
|
+
source: "steam",
|
|
459
|
+
workflow,
|
|
460
|
+
evidenceType,
|
|
461
|
+
sections,
|
|
462
|
+
query,
|
|
463
|
+
sampling,
|
|
464
|
+
searchRequest: {
|
|
465
|
+
path: `/api/v1/lists/${encodeURIComponent(listId)}/games`,
|
|
466
|
+
query
|
|
467
|
+
},
|
|
468
|
+
analysisQuery: sections.length
|
|
469
|
+
? {
|
|
470
|
+
...pickQuery(options, {
|
|
471
|
+
locale: "locale"
|
|
472
|
+
}),
|
|
473
|
+
sections: sections.join(",")
|
|
474
|
+
}
|
|
475
|
+
: undefined
|
|
476
|
+
};
|
|
477
|
+
}
|
|
361
478
|
const query = {
|
|
362
479
|
sort: "reviews_desc",
|
|
363
480
|
limit: sampling?.pageLimit ?? DEFAULT_COHORT_WORKFLOW_LIMIT,
|
|
364
|
-
...buildGameSearchQuery(options)
|
|
481
|
+
...buildGameSearchQuery(options, { includeTotal: false })
|
|
365
482
|
};
|
|
366
483
|
if (sampling && options.limit == null) query.limit = sampling.pageLimit;
|
|
367
484
|
return {
|
|
@@ -372,6 +489,7 @@ function buildCohortEvidenceRequest({ workflow, evidenceType, sections, options
|
|
|
372
489
|
sections,
|
|
373
490
|
query,
|
|
374
491
|
sampling,
|
|
492
|
+
countRequest: buildOptionalGameCountRequest(options),
|
|
375
493
|
searchRequest: {
|
|
376
494
|
path: "/api/v1/steam/games",
|
|
377
495
|
query
|
|
@@ -410,7 +528,7 @@ function pickQuery(options, mapping) {
|
|
|
410
528
|
return query;
|
|
411
529
|
}
|
|
412
530
|
|
|
413
|
-
function buildGameSearchQuery(options) {
|
|
531
|
+
function buildGameSearchQuery(options, { includeTotal = Boolean(options["include-total"]) } = {}) {
|
|
414
532
|
const query = {
|
|
415
533
|
...buildGameCountQuery(options),
|
|
416
534
|
...pickQuery(options, {
|
|
@@ -418,12 +536,21 @@ function buildGameSearchQuery(options) {
|
|
|
418
536
|
limit: "limit",
|
|
419
537
|
offset: "offset",
|
|
420
538
|
locale: "locale"
|
|
421
|
-
})
|
|
539
|
+
}),
|
|
540
|
+
includeTotal
|
|
422
541
|
};
|
|
423
542
|
|
|
424
543
|
return query;
|
|
425
544
|
}
|
|
426
545
|
|
|
546
|
+
function buildOptionalGameCountRequest(options) {
|
|
547
|
+
if (!options["include-total"]) return undefined;
|
|
548
|
+
return {
|
|
549
|
+
path: "/api/v1/steam/games/count",
|
|
550
|
+
query: buildGameCountQuery(options)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
427
554
|
function buildGameCountQuery(options) {
|
|
428
555
|
const query = pickQuery(options, {
|
|
429
556
|
query: "q",
|
|
@@ -646,6 +773,73 @@ function assertResolveQuery(value) {
|
|
|
646
773
|
}
|
|
647
774
|
}
|
|
648
775
|
|
|
776
|
+
function assertRequiredOption(value, optionName) {
|
|
777
|
+
if (value == null || String(value).trim() === "") {
|
|
778
|
+
writeJsonError({
|
|
779
|
+
code: "CLI_USAGE_ERROR",
|
|
780
|
+
message: `Missing required option ${optionName}`
|
|
781
|
+
});
|
|
782
|
+
process.exit(EXIT_USAGE);
|
|
783
|
+
}
|
|
784
|
+
return String(value);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function assertListKind(value) {
|
|
788
|
+
if (value !== "manual" && value !== "filter") {
|
|
789
|
+
writeJsonError({
|
|
790
|
+
code: "CLI_USAGE_ERROR",
|
|
791
|
+
message: "--kind must be manual or filter"
|
|
792
|
+
});
|
|
793
|
+
process.exit(EXIT_USAGE);
|
|
794
|
+
}
|
|
795
|
+
return value;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function assertListId(value, usage) {
|
|
799
|
+
if (value == null || String(value).trim() === "") {
|
|
800
|
+
writeJsonError({
|
|
801
|
+
code: "CLI_USAGE_ERROR",
|
|
802
|
+
message: `Missing list id. Usage: ${usage}`
|
|
803
|
+
});
|
|
804
|
+
process.exit(EXIT_USAGE);
|
|
805
|
+
}
|
|
806
|
+
return String(value);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function parseJsonOption(value, optionName) {
|
|
810
|
+
if (value == null) {
|
|
811
|
+
writeJsonError({
|
|
812
|
+
code: "CLI_USAGE_ERROR",
|
|
813
|
+
message: `${optionName} is required`
|
|
814
|
+
});
|
|
815
|
+
process.exit(EXIT_USAGE);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const parsed = JSON.parse(String(value));
|
|
819
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("expected object");
|
|
820
|
+
return parsed;
|
|
821
|
+
} catch {
|
|
822
|
+
writeJsonError({
|
|
823
|
+
code: "CLI_USAGE_ERROR",
|
|
824
|
+
message: `${optionName} must be a JSON object`
|
|
825
|
+
});
|
|
826
|
+
process.exit(EXIT_USAGE);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function parseAppIdsOption(value) {
|
|
831
|
+
const appids = parseCsvString(assertRequiredOption(value, "--app-ids"))
|
|
832
|
+
.map((item) => Number(item));
|
|
833
|
+
if (appids.length === 0 || appids.some((appid) => !Number.isSafeInteger(appid) || appid <= 0)) {
|
|
834
|
+
writeJsonError({
|
|
835
|
+
code: "CLI_USAGE_ERROR",
|
|
836
|
+
message: "--app-ids must be a comma-separated list of positive integers"
|
|
837
|
+
});
|
|
838
|
+
process.exit(EXIT_USAGE);
|
|
839
|
+
}
|
|
840
|
+
return appids;
|
|
841
|
+
}
|
|
842
|
+
|
|
649
843
|
function assertCompareExternalIds(values) {
|
|
650
844
|
if (values.length < 2) {
|
|
651
845
|
writeJsonError({
|