@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 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 beta CLI:
9
+ Install the CLI:
10
10
 
11
11
  ```bash
12
- npm install -g @witchpot/gmc@beta
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@beta --version
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. Install it
68
- so Claude Code (and similar tools) load it automatically:
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
- edition (references inlined) to your project brief:
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 resolve --source steam --query Timberborn --json
91
- gmc games detail --source steam 1062090 --json
92
- gmc campaigns signals --source steam 1062090 --json
93
- gmc games benchmark --source steam --tags city_builder,automation --reviews-min 1000 --preset summary --json
94
- gmc showcases fit --source steam 1062090 --json
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.1.8",
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
  }
@@ -33,14 +33,40 @@ const smokeCommands = [
33
33
  validate: (payload) => requireSuccessEnvelope(payload, ["data.version", "data.cliCommands"])
34
34
  },
35
35
  {
36
- name: "resolve Timberborn",
37
- args: ["games", "resolve", "--source", "steam", "--query", "Timberborn", "--json"],
38
- validate: (payload) => requireComposedEnvelope(payload, ["data.candidates", "data.resolution"])
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: "detail Timberborn",
42
- args: ["games", "detail", "--source", "steam", "1062090", "--json"],
43
- validate: (payload) => requireSuccessEnvelope(payload, ["data.title"])
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 fit Timberborn",
64
- args: ["showcases", "fit", "--source", "steam", "1062090", "--json"],
65
- validate: (payload) => requireComposedEnvelope(payload, ["data.matches", "data.summary"])
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 the install command for the closed-beta package once it is published."
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 ${CLI_BETA_PACKAGE_NAME}@beta`,
194
- npmExec: `npx ${CLI_BETA_PACKAGE_NAME}@beta --version`
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 in the closed-beta contract.",
198
- "A future gmc update --apply command can run the package-manager update after the npm distribution channel is finalized."
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.1.8";
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 beta commands:
64
+ Supported commands:
60
65
  ${SUPPORTED_BETA_COMMANDS.map((command) => ` ${command}`).join("\n")}
61
66
 
62
- Provisional beta commands:
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 query = {
202
- sort: "reviews_desc",
203
- limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
204
- ...buildGameSearchQuery(options)
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({