@witchpot/gmc 0.1.6 → 0.1.8
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 +18 -0
- package/README.md +21 -0
- package/package.json +2 -1
- package/scripts/check-release-readiness.mjs +17 -1
- package/scripts/smoke-pack.mjs +17 -0
- package/skill/gmc-analysis/CHANGELOG.md +29 -0
- package/skill/gmc-analysis/SKILL.md +97 -0
- package/skill/gmc-analysis/references/pitfalls.md +72 -0
- package/skill/gmc-analysis/references/quota.md +27 -0
- package/skill/gmc-analysis/references/recipes.md +101 -0
- package/src/args.js +5 -1
- package/src/config.js +13 -0
- package/src/constants.js +4 -1
- package/src/help.js +3 -0
- package/src/main.js +4 -0
- package/src/skill.js +132 -0
- package/src/skill.test.js +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.8 - 2026-06-12
|
|
6
|
+
|
|
7
|
+
- Updated the bundled `gmc-analysis` skill to v0.2.0 with the learnings from
|
|
8
|
+
the first price-band field test: camelCase-query to kebab-case-flag
|
|
9
|
+
mapping note, zero-review rating artifact pitfall, discounted snapshot
|
|
10
|
+
price pitfall, price-question routing to explicit-bucket aggregates, and
|
|
11
|
+
the evidence-pack quota location exception.
|
|
12
|
+
|
|
13
|
+
## 0.1.7 - 2026-06-12
|
|
14
|
+
|
|
15
|
+
- Bundled the `gmc-analysis` Agent Skill (v0.1.0): analysis recipes, pivot
|
|
16
|
+
heuristics, quota etiquette, and honesty rules for Claude Code / Codex
|
|
17
|
+
agents, seeded from the 2026-06-12 field test.
|
|
18
|
+
- Added `gmc skill install` (project `.claude/skills/` or `--global`),
|
|
19
|
+
`gmc skill print --format agents-md` (self-contained AGENTS.md section
|
|
20
|
+
with references inlined), and `gmc skill status` (packaged vs installed
|
|
21
|
+
version check with stale warnings).
|
|
22
|
+
|
|
5
23
|
## 0.1.6 - 2026-06-12
|
|
6
24
|
|
|
7
25
|
- Added `gmcp` and `game-market-copilot` binary aliases. Oh My Zsh's git
|
package/README.md
CHANGED
|
@@ -61,6 +61,27 @@ gmc auth status --json
|
|
|
61
61
|
The CLI never prints the raw key or credential after login. Status and config output show only
|
|
62
62
|
a non-secret prefix.
|
|
63
63
|
|
|
64
|
+
## Agent Skill
|
|
65
|
+
|
|
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:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gmc skill install # writes .claude/skills/gmc-analysis (project)
|
|
72
|
+
gmc skill install --global # or ~/.claude/skills/gmc-analysis
|
|
73
|
+
gmc skill status --json # packaged vs installed versions
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For Codex and other AGENTS.md-based agents, append the self-contained
|
|
77
|
+
edition (references inlined) to your project brief:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
gmc skill print --format agents-md >> AGENTS.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Re-run install/print after `npm update` to pick up newer skill versions.
|
|
84
|
+
|
|
64
85
|
## First Queries
|
|
65
86
|
|
|
66
87
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchpot/gmc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tools for Game Market Copilot market research.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"bin",
|
|
32
32
|
"src",
|
|
33
33
|
"scripts",
|
|
34
|
+
"skill",
|
|
34
35
|
"README.md",
|
|
35
36
|
"CHANGELOG.md"
|
|
36
37
|
],
|
|
@@ -13,11 +13,16 @@ const requiredFiles = [
|
|
|
13
13
|
"src/main.js",
|
|
14
14
|
"src/requests.js",
|
|
15
15
|
"src/workflows.js",
|
|
16
|
+
"skill/gmc-analysis/SKILL.md",
|
|
17
|
+
"skill/gmc-analysis/references/recipes.md",
|
|
18
|
+
"skill/gmc-analysis/references/pitfalls.md",
|
|
19
|
+
"skill/gmc-analysis/references/quota.md",
|
|
20
|
+
"skill/gmc-analysis/CHANGELOG.md",
|
|
16
21
|
"README.md",
|
|
17
22
|
"CHANGELOG.md"
|
|
18
23
|
];
|
|
19
24
|
|
|
20
|
-
const requiredFileGlobs = ["bin", "src", "scripts", "README.md", "CHANGELOG.md"];
|
|
25
|
+
const requiredFileGlobs = ["bin", "src", "scripts", "skill", "README.md", "CHANGELOG.md"];
|
|
21
26
|
const forbiddenPatterns = [
|
|
22
27
|
{ pattern: /@supabase\//, message: "CLI package must not import Supabase SDKs." },
|
|
23
28
|
{ pattern: /@clerk\//, message: "CLI package must not import Clerk SDKs." },
|
|
@@ -94,6 +99,17 @@ if (!changelog.includes(`## ${packageJson.version} -`)) {
|
|
|
94
99
|
failures.push(`CHANGELOG.md must include a section for ${packageJson.version}.`);
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
const skillSource = readFileSync(path.join(packageDir, "skill/gmc-analysis/SKILL.md"), "utf8");
|
|
103
|
+
const skillVersionMatch = skillSource.match(/^\s*version:\s*"?([0-9][0-9A-Za-z.\-]*)"?\s*$/m);
|
|
104
|
+
if (!skillVersionMatch) {
|
|
105
|
+
failures.push("skill/gmc-analysis/SKILL.md must declare metadata.version in its frontmatter.");
|
|
106
|
+
} else {
|
|
107
|
+
const skillChangelog = readFileSync(path.join(packageDir, "skill/gmc-analysis/CHANGELOG.md"), "utf8");
|
|
108
|
+
if (!skillChangelog.includes(`## ${skillVersionMatch[1]} -`)) {
|
|
109
|
+
failures.push(`skill CHANGELOG.md must include a section for skill version ${skillVersionMatch[1]}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
for (const file of sourceFiles) {
|
|
98
114
|
const source = readFileSync(path.join(packageDir, file), "utf8");
|
|
99
115
|
for (const forbidden of forbiddenPatterns) {
|
package/scripts/smoke-pack.mjs
CHANGED
|
@@ -65,6 +65,23 @@ try {
|
|
|
65
65
|
throw new Error("Packed gmc config get --json did not print the expected config payload");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const skillStatusResult = run(gmcBin, ["skill", "status", "--json"], {
|
|
69
|
+
env: { GMC_CONFIG_PATH: configPath },
|
|
70
|
+
capture: true
|
|
71
|
+
});
|
|
72
|
+
const skillStatus = JSON.parse(skillStatusResult.stdout);
|
|
73
|
+
if (!skillStatus?.data?.packaged?.version) {
|
|
74
|
+
throw new Error("Packed gmc skill status did not report a packaged skill version");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const skillPrintResult = run(gmcBin, ["skill", "print", "--format", "agents-md"], {
|
|
78
|
+
env: { GMC_CONFIG_PATH: configPath },
|
|
79
|
+
capture: true
|
|
80
|
+
});
|
|
81
|
+
if (!skillPrintResult.stdout.includes("GMC Analysis") || !skillPrintResult.stdout.includes("Pitfalls")) {
|
|
82
|
+
throw new Error("Packed gmc skill print did not emit the inlined agents-md section");
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
// gmc is shadowed by Oh My Zsh's `git merge --continue` alias, so the
|
|
69
86
|
// package ships alias binaries; make sure npm actually installed them.
|
|
70
87
|
for (const aliasName of ["gmcp", "game-market-copilot"]) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# gmc-analysis skill changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 - 2026-06-12
|
|
4
|
+
|
|
5
|
+
- Folded in the five learnings from the first field test (2025 city
|
|
6
|
+
builders, price-band vs rating/review analysis; 7/7 rubric pass):
|
|
7
|
+
- setup: documented the camelCase-query -> kebab-case-flag mapping and
|
|
8
|
+
that per-command `--help` is currently generic;
|
|
9
|
+
- new pitfall P5: zero-review games aggregate as rating 0 — pair rating
|
|
10
|
+
metrics with `--reviews-min`, derive per-bucket hit rates from counts;
|
|
11
|
+
- new pitfall P6: snapshot prices include active discounts — check the
|
|
12
|
+
`discount` field before price-band conclusions;
|
|
13
|
+
- recipe R4: price/distribution questions route to aggregate with
|
|
14
|
+
explicit buckets first; `cohort pricing` is page-bounded texture only;
|
|
15
|
+
- quota: composed evidence packs carry quota under
|
|
16
|
+
`meta.upstreamRequests[].entitlements`, not at the top level.
|
|
17
|
+
|
|
18
|
+
## 0.1.0 - 2026-06-12
|
|
19
|
+
|
|
20
|
+
- Initial release, seeded from the 2026-06-12 field test (roguelike cohort
|
|
21
|
+
complaint analysis over 114 games):
|
|
22
|
+
- core workflow: coverage pre-estimation -> bounded workflow -> pivot ->
|
|
23
|
+
exemplar drill-down;
|
|
24
|
+
- pitfalls P1-P5 including the cross-game pattern aggregation pivot and
|
|
25
|
+
the coverage-vs-topics distinction;
|
|
26
|
+
- full-cohort map-reduce recipe with `--fields` projection and subagent
|
|
27
|
+
chunking;
|
|
28
|
+
- quota etiquette and honesty rules (skippedReasons, locked/preview,
|
|
29
|
+
bounded-evidence caveats).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gmc-analysis
|
|
3
|
+
description: >-
|
|
4
|
+
Use when analyzing Steam game market data with the gmc CLI (Game Market
|
|
5
|
+
Copilot) — cross-game cohort analysis (common complaints, marketing
|
|
6
|
+
patterns, pricing), single-title deep dives, market sizing, or any question
|
|
7
|
+
about Steam games, reviews, creators, showcases, or campaigns that gmc can
|
|
8
|
+
answer.
|
|
9
|
+
metadata:
|
|
10
|
+
version: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# GMC Analysis
|
|
14
|
+
|
|
15
|
+
The gmc CLI is a JSON-first client for the Game Market Copilot API (Steam
|
|
16
|
+
market analytics). The division of labor: the server owns data and access
|
|
17
|
+
gates, the CLI owns mechanical collection and bounded aggregation, and YOU
|
|
18
|
+
(the agent) own semantic interpretation. The CLI will not cluster meanings
|
|
19
|
+
across games — that is your job.
|
|
20
|
+
|
|
21
|
+
## Setup check (run once per session)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gmc --version # gmcp / game-market-copilot are the same CLI
|
|
25
|
+
gmc auth status --json # anonymous => redacted previews + tiny quota
|
|
26
|
+
gmc schema --json # the authoritative command/endpoint reference
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Never guess flags: `gmc schema --json` is the source of truth for commands
|
|
30
|
+
and parameters. Endpoint query params are camelCase and the matching CLI
|
|
31
|
+
flag is the kebab-case equivalent (`priceMin` -> `--price-min`,
|
|
32
|
+
`reviewsGt` -> `--reviews-gt`). Per-command `--help` currently prints one
|
|
33
|
+
generic usage list, so trust the schema. Always pass `--json`.
|
|
34
|
+
|
|
35
|
+
## Core workflow for cohort questions
|
|
36
|
+
|
|
37
|
+
1. **Size the cohort and its research coverage first** (1-2 requests):
|
|
38
|
+
`gmc games count --source steam <filters> --json`, then again with
|
|
39
|
+
`--coverage full,partial`. Deep analysis exists only for researched
|
|
40
|
+
titles; most of the catalog is `basic` (catalog + snapshot only). This
|
|
41
|
+
tells you the denominator and the fan-out cost before you spend quota.
|
|
42
|
+
2. **Try the bounded cross-game workflow** that matches the question
|
|
43
|
+
(`gmc reviews patterns`, `gmc marketing patterns`, `gmc cohort ...`) with
|
|
44
|
+
`--coverage full,partial` in the filters.
|
|
45
|
+
3. **Pivot when the workflow under-delivers** — see the pitfalls reference.
|
|
46
|
+
The most common pivot: per-game `gmc games analysis --sections reviews`
|
|
47
|
+
over the cohort, then cluster the named theme labels yourself.
|
|
48
|
+
4. **Drill into exemplars**: `gmc games detail`, `gmc games analysis`,
|
|
49
|
+
`gmc games success-report`, `gmc campaigns signals` on 2-5 representative
|
|
50
|
+
titles to add texture to the synthesis.
|
|
51
|
+
|
|
52
|
+
For cohorts beyond one page, and for the full-cohort map-reduce procedure
|
|
53
|
+
(pagination, bounded concurrency, `--fields` projection, subagent chunking),
|
|
54
|
+
follow the recipes reference.
|
|
55
|
+
|
|
56
|
+
## Hard rules
|
|
57
|
+
|
|
58
|
+
- **Missing data is not absent sentiment.** Report `cohort.usableGames`,
|
|
59
|
+
`cohort.skippedGames`, and `cohort.skippedReasons`. "No review topics
|
|
60
|
+
collected" must never be presented as "players have no complaints".
|
|
61
|
+
- **Locked is locked.** When `meta.entitlements.locked` or `preview: true`
|
|
62
|
+
appears, present what you got as a preview, name the lock key (e.g.
|
|
63
|
+
`success_report.full`), and never imply you read the full content.
|
|
64
|
+
- **Bounded evidence, not market truth.** Carry the CLI's own caveats:
|
|
65
|
+
evidence packs are sampled/returned-page evidence. Always state
|
|
66
|
+
denominators ("87 of 107 analyzed titles") and preserve `warnings`.
|
|
67
|
+
- **Separate API facts from your synthesis.** Quote theme labels and player
|
|
68
|
+
counts as data; label your clustering and conclusions as your analysis.
|
|
69
|
+
- **Pace your quota.** Read `meta.entitlements.quota` (`used`/`limit`)
|
|
70
|
+
before fanning out and keep headroom; on HTTP 429 respect `Retry-After`.
|
|
71
|
+
Details in the quota reference.
|
|
72
|
+
- Preserve `meta.requestId` values for anything you may need to report.
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
- `references/recipes.md` — step-by-step playbooks (cohort complaints,
|
|
77
|
+
full-cohort map-reduce, single-title deep dive, market sizing).
|
|
78
|
+
- `references/pitfalls.md` — known failure modes and the correct pivots.
|
|
79
|
+
- `references/quota.md` — quota model and cost estimation.
|
|
80
|
+
|
|
81
|
+
(In the AGENTS.md edition of this skill the references are inlined below.)
|
|
82
|
+
|
|
83
|
+
## When recipes fail
|
|
84
|
+
|
|
85
|
+
If a documented recipe fails or you discover a better workaround, record a
|
|
86
|
+
learning entry so the skill can be improved:
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
- Source: field usage
|
|
90
|
+
- Task: <the analysis question>
|
|
91
|
+
- What happened: <observed behavior, requestIds>
|
|
92
|
+
- Gap or win: <missing knowledge / what worked>
|
|
93
|
+
- Proposed rule: <one-sentence candidate rule>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Suggest the user submits it through the GMC feedback channel (template in
|
|
97
|
+
the gmc user guide).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Pitfalls and pivots
|
|
2
|
+
|
|
3
|
+
Known failure modes observed in real field tests, with the correct response.
|
|
4
|
+
Entries carry an observation date; behavior may improve in later releases.
|
|
5
|
+
|
|
6
|
+
## P1. `reviews patterns` may aggregate nothing across games (2026-06)
|
|
7
|
+
|
|
8
|
+
Cross-game pattern keys are derived from per-game topic labels and cluster
|
|
9
|
+
IDs, which rarely match between games. A run where `data.patterns` has zero
|
|
10
|
+
entries with `appearsInGames >= 2` is an EXPECTED outcome on many cohorts —
|
|
11
|
+
it does not mean the service is broken and it does not mean the games share
|
|
12
|
+
no complaints.
|
|
13
|
+
|
|
14
|
+
**Pivot**: per-game `gmc games analysis --sections reviews`, then cluster
|
|
15
|
+
the `negativeThemes`/`positiveThemes` labels yourself (recipe R1/R2).
|
|
16
|
+
|
|
17
|
+
## P2. Research coverage does not guarantee review topics (2026-06)
|
|
18
|
+
|
|
19
|
+
A game with `coverage: full` can still return no review topics;
|
|
20
|
+
`cohort.skippedReasons.no_matching_review_topics` is common (observed 10 of
|
|
21
|
+
15 on one researched cohort). Theme-level analysis
|
|
22
|
+
(`analysis --sections reviews`) has broader coverage than topic explorer
|
|
23
|
+
data.
|
|
24
|
+
|
|
25
|
+
**Pivot**: prefer themes over topics for cohort work; report skipped games
|
|
26
|
+
as "topics not collected", never as "no complaints found".
|
|
27
|
+
|
|
28
|
+
## P3. Preview and locked payloads
|
|
29
|
+
|
|
30
|
+
Anonymous and Free callers receive redacted previews. Markers:
|
|
31
|
+
`meta.entitlements.locked` (lock keys like `success_report.full`,
|
|
32
|
+
`reviews.topics.full`), `preview: true` fields, truncated summaries.
|
|
33
|
+
|
|
34
|
+
**Rule**: present previews as previews, name the lock, and suggest the plan
|
|
35
|
+
that unlocks it if the user asks. Never extrapolate locked content.
|
|
36
|
+
|
|
37
|
+
## P4. Anonymous sessions look broken but are just unauthenticated
|
|
38
|
+
|
|
39
|
+
Without `gmc login` / `GMC_API_KEY`, deep sections come back redacted and
|
|
40
|
+
the quota is tiny, which can read as "the API has no data".
|
|
41
|
+
|
|
42
|
+
**Rule**: run `gmc auth status --json` first; if `authMode` is `anonymous`,
|
|
43
|
+
say so in your answer and scope expectations to public preview data.
|
|
44
|
+
|
|
45
|
+
## P5. Zero-review games carry rating 0 in aggregates (2026-06)
|
|
46
|
+
|
|
47
|
+
Unconditional `avg_rating` / `median_rating` in low-traction segments (cheap
|
|
48
|
+
or free price buckets) is crushed by zero-review games aggregating as
|
|
49
|
+
rating 0 (observed: a free bucket with avg_rating 11.1). This misreads as
|
|
50
|
+
"cheap games are hated" when it actually means "most cheap games have no
|
|
51
|
+
reviews".
|
|
52
|
+
|
|
53
|
+
**Pivot**: pair rating metrics with `--reviews-min` (e.g. 10). For "share of
|
|
54
|
+
well-rated games per bucket", run `--metric count` twice over the same
|
|
55
|
+
bucket spec — once unconditional, once with `--reviews-min 10
|
|
56
|
+
--rating-min 80` — and divide.
|
|
57
|
+
|
|
58
|
+
## P6. Snapshot prices include active discounts (2026-06)
|
|
59
|
+
|
|
60
|
+
`price` is the current store price, not the launch/list price; a discounted
|
|
61
|
+
title lands in a lower price bucket (observed: a $34.99 list price counted
|
|
62
|
+
as $23.44 at -33%). Game rows carry a `discount` field.
|
|
63
|
+
|
|
64
|
+
**Rule**: when price-band conclusions matter, check `discount` on the
|
|
65
|
+
exemplar rows and caveat bucket assignments, especially during sale seasons.
|
|
66
|
+
|
|
67
|
+
## P7. Don't paginate by brute force
|
|
68
|
+
|
|
69
|
+
List endpoints cap at `limit=100`. For "how many" questions use
|
|
70
|
+
`gmc games count`; for distributions use `gmc games aggregate` — both are
|
|
71
|
+
single-request server aggregates. Reserve pagination loops for when you
|
|
72
|
+
actually need the rows (recipe R2).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Quota etiquette
|
|
2
|
+
|
|
3
|
+
Quotas are per workspace, per UTC day, enforced for bearer credentials
|
|
4
|
+
(API keys / CLI credentials). As of 2026-06: starter 1,000/day, pro
|
|
5
|
+
10,000/day; anonymous public preview is far smaller. Treat these numbers as
|
|
6
|
+
indicative — the response metadata is the source of truth.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
10
|
+
1. **Read before you spend.** Every bearer response carries
|
|
11
|
+
`meta.entitlements.quota` = `{ used, limit }`. Check it at the start of a
|
|
12
|
+
session and after each fan-out. Exception: CLI-composed evidence packs
|
|
13
|
+
(`cohort ...`, `reviews patterns`, `games benchmark`, ...) expose quota
|
|
14
|
+
per upstream call instead — read the last entry of
|
|
15
|
+
`meta.upstreamRequests[].entitlements.quota`.
|
|
16
|
+
2. **Estimate fan-out cost first.** A cohort sweep costs roughly
|
|
17
|
+
`search pages + (1 per game per section call)`. A 114-game cohort with one
|
|
18
|
+
analysis call each ~= 120 requests. Compare against remaining quota and
|
|
19
|
+
keep at least ~20% headroom for drill-downs and retries.
|
|
20
|
+
3. **Shrink the cohort before shrinking the question.** Use
|
|
21
|
+
`--coverage full,partial` and tighter filters so requests are not spent on
|
|
22
|
+
titles that would be skipped anyway.
|
|
23
|
+
4. **On HTTP 429 (`QUOTA_EXCEEDED`)**: stop fanning out, respect the
|
|
24
|
+
`Retry-After` header / `error.retryAfterSeconds` (the window resets at UTC
|
|
25
|
+
midnight), and tell the user how much of the analysis completed.
|
|
26
|
+
5. **Prefer server aggregates** (`count`, `aggregate`) over row pagination —
|
|
27
|
+
they answer sizing questions in one request.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Placeholders use `<...>`. Always add `--json`. Commands and parameters are
|
|
4
|
+
authoritative in `gmc schema --json`; these recipes encode strategy, not
|
|
5
|
+
syntax.
|
|
6
|
+
|
|
7
|
+
## R1. Cohort complaints / praises ("what do players dislike about <cohort>?")
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Denominators (2 requests)
|
|
11
|
+
gmc games count --source steam <filters> --json
|
|
12
|
+
gmc games count --source steam <filters> --coverage full,partial --json
|
|
13
|
+
|
|
14
|
+
# 2. Bounded cross-game attempt (1 + N requests)
|
|
15
|
+
gmc reviews patterns --source steam <filters> --coverage full,partial \
|
|
16
|
+
--limit 15 --max-topics-per-game 8 --preset summary --json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Read `data.patterns[].appearsInGames`. If no pattern appears in 2+ games
|
|
20
|
+
(common — see pitfalls P1), pivot to per-game themes:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 3. Per-game named themes (N requests; theme labels are written text)
|
|
24
|
+
gmc games analysis --source steam <appid> --sections reviews --json
|
|
25
|
+
# extract sections.reviews.negativeThemes[]: {label, frequency, playerCount}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Cluster the theme labels yourself (semantic grouping is your job), then
|
|
29
|
+
report: cluster -> games count / usable games, with 2-3 quoted example
|
|
30
|
+
labels per cluster and the skipped/missing counts.
|
|
31
|
+
|
|
32
|
+
## R2. Full-cohort map-reduce (cohort larger than one page)
|
|
33
|
+
|
|
34
|
+
Cost: ~(pages + N) requests. Check quota headroom first (see quota.md).
|
|
35
|
+
|
|
36
|
+
1. **Collect the cohort**: page `gmc games search <filters> --coverage
|
|
37
|
+
full,partial --sort reviews_desc --limit 100 --offset <0,100,...>
|
|
38
|
+
--fields appid,title,reviews --json` until `pagination.nextOffset` is
|
|
39
|
+
null.
|
|
40
|
+
2. **Fan out** `gmc games analysis --source steam <appid> --sections
|
|
41
|
+
reviews --json` per game with bounded concurrency (<= 8 parallel
|
|
42
|
+
processes). Write each response to a temp file; tolerate individual
|
|
43
|
+
failures and count them.
|
|
44
|
+
3. **Project before reading**: extract only what you need (e.g.
|
|
45
|
+
`negativeThemes` label/frequency/playerCount) into one compact JSON/NDJSON
|
|
46
|
+
file. A 100+ game cohort fits in a few tens of KB after projection.
|
|
47
|
+
4. **Cluster semantically** over the projected labels. Crude keyword
|
|
48
|
+
bucketing is acceptable as a first pass, but review the "other" bucket
|
|
49
|
+
and misclassifications yourself before reporting.
|
|
50
|
+
5. **Report with denominators**: cohort size, games with analysis, games
|
|
51
|
+
skipped (by reason), cluster -> game counts, examples, and the caveat
|
|
52
|
+
that this is researched-cohort evidence, not full-market measurement.
|
|
53
|
+
|
|
54
|
+
When the cohort exceeds ~100 games or projected evidence would overflow your
|
|
55
|
+
context, chunk the appid list across subagents (e.g. 20 games each), have
|
|
56
|
+
each return its projected themes, then merge and cluster in the lead agent.
|
|
57
|
+
|
|
58
|
+
## R3. Single-title deep dive ("why did <game> work?")
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
gmc games resolve --source steam --query <title> --json # if appid unknown
|
|
62
|
+
gmc games detail --source steam <appid> --json
|
|
63
|
+
gmc games analysis --source steam <appid> --sections reviews,marketing,creators,media --json
|
|
64
|
+
gmc games success-report --source steam <appid> --json # long-form narrative
|
|
65
|
+
gmc campaigns signals --source steam <appid> --json # cross-channel timeline
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Use `--fields` on detail to keep output small. Respect preview/locked rules
|
|
69
|
+
(pitfalls P3) on every response.
|
|
70
|
+
|
|
71
|
+
## R4. Market sizing and distribution
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
gmc games count --source steam <filters> --json
|
|
75
|
+
gmc games aggregate --source steam --metric count,median_reviews \
|
|
76
|
+
--group-by release_year <filters> --json
|
|
77
|
+
gmc games aggregate --source steam --metric count --group-by coverage <filters> --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`data.basis = server_aggregate` means market-wide for the filter set (within
|
|
81
|
+
GMC source coverage) — unlike evidence packs, these are not page-bounded.
|
|
82
|
+
|
|
83
|
+
**Price/distribution questions belong here, not in `cohort pricing`.** Run
|
|
84
|
+
the aggregate with explicit buckets first (e.g.
|
|
85
|
+
`--bucket price:0,5,10,15,20,25,30,40,60`); `gmc cohort pricing` is
|
|
86
|
+
returned-page (top ~20) texture, useful only as a supplement and must be
|
|
87
|
+
labeled as page-bounded evidence.
|
|
88
|
+
|
|
89
|
+
Per-bucket "hit rate" pattern (one request per condition): pair the same
|
|
90
|
+
bucket spec with shared filters — `--reviews-min 500 --metric count` gives
|
|
91
|
+
reach per price band; `--reviews-min 10 --rating-min 80 --metric count`
|
|
92
|
+
gives the well-rated share. Divide by the unconditional bucket counts.
|
|
93
|
+
Never read unconditional rating aggregates in low-traction buckets
|
|
94
|
+
(pitfall P5).
|
|
95
|
+
|
|
96
|
+
## Output contract for every analysis
|
|
97
|
+
|
|
98
|
+
Always include in your final answer: the cohort definition and size, how many
|
|
99
|
+
games carried the evidence (usable vs skipped, with reasons), which plan/
|
|
100
|
+
entitlement context applied, notable `warnings`, and a clear line between
|
|
101
|
+
data and your synthesis.
|
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"]);
|
|
4
|
+
export const BOOLEAN_OPTIONS = new Set(["json", "help", "version", "no-open", "global"]);
|
|
5
5
|
|
|
6
6
|
export function parseArgs(args) {
|
|
7
7
|
const command = [];
|
|
@@ -60,6 +60,7 @@ function isCommandToken(command, token) {
|
|
|
60
60
|
"login",
|
|
61
61
|
"logout",
|
|
62
62
|
"config",
|
|
63
|
+
"skill",
|
|
63
64
|
"update",
|
|
64
65
|
"version"
|
|
65
66
|
].includes(token);
|
|
@@ -70,6 +71,9 @@ function isCommandToken(command, token) {
|
|
|
70
71
|
if (command[0] === "config") {
|
|
71
72
|
return ["get", "set"].includes(token);
|
|
72
73
|
}
|
|
74
|
+
if (command[0] === "skill") {
|
|
75
|
+
return ["install", "print", "status"].includes(token);
|
|
76
|
+
}
|
|
73
77
|
if (command[0] === "games") {
|
|
74
78
|
return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "success-report"].includes(token);
|
|
75
79
|
}
|
package/src/config.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { matches } from "./args.js";
|
|
14
14
|
import { writeJsonError } from "./output.js";
|
|
15
15
|
import { runBrowserLogin } from "./browser-login.js";
|
|
16
|
+
import { handleSkillInstall, handleSkillPrint, handleSkillStatus } from "./skill.js";
|
|
16
17
|
|
|
17
18
|
export function loadConfig() {
|
|
18
19
|
const configPath = resolveConfigPath(process.env.GMC_CONFIG_PATH);
|
|
@@ -89,6 +90,18 @@ function resolveAuthMode({ envApiKey, envCliCredential, fileConfig }) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export async function handleLocalCommand(command, options, positional, config) {
|
|
93
|
+
if (matches(command, ["skill", "install"])) {
|
|
94
|
+
return handleSkillInstall(options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (matches(command, ["skill", "print"])) {
|
|
98
|
+
return handleSkillPrint(options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (matches(command, ["skill", "status"])) {
|
|
102
|
+
return handleSkillStatus();
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
if (matches(command, ["login"])) {
|
|
93
106
|
const apiKey = options["api-key"];
|
|
94
107
|
const apiBaseUrl = options["api-base-url"] || options["base-url"] || config.apiBaseUrl;
|
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.8";
|
|
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");
|
|
@@ -19,6 +19,9 @@ export const SUPPORTED_BETA_COMMANDS = [
|
|
|
19
19
|
"gmc schema --json",
|
|
20
20
|
"gmc --version",
|
|
21
21
|
"gmc update --json",
|
|
22
|
+
"gmc skill install --json",
|
|
23
|
+
"gmc skill print --format agents-md",
|
|
24
|
+
"gmc skill status --json",
|
|
22
25
|
"gmc games search --source steam --query <text> --json",
|
|
23
26
|
"gmc games count --source steam --release-year <year> --reviews-gt <number> --json",
|
|
24
27
|
"gmc games aggregate --source steam --metric count,median_reviews --group-by release_year --json",
|
package/src/help.js
CHANGED
|
@@ -21,6 +21,9 @@ Usage:
|
|
|
21
21
|
gmc update --json
|
|
22
22
|
gmc config get --json
|
|
23
23
|
gmc config set apiBaseUrl <url>
|
|
24
|
+
gmc skill install [--global] --json
|
|
25
|
+
gmc skill print --format agents-md
|
|
26
|
+
gmc skill status --json
|
|
24
27
|
gmc schema --json
|
|
25
28
|
gmc games search --source steam --query <text> --json
|
|
26
29
|
gmc games count --source steam --release-year <year> --reviews-gt <number> --json
|
package/src/main.js
CHANGED
|
@@ -48,6 +48,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
48
48
|
const config = loadConfig();
|
|
49
49
|
const localOutput = await handleLocalCommand(command, options, positional, config);
|
|
50
50
|
if (localOutput) {
|
|
51
|
+
if (typeof localOutput.raw === "string") {
|
|
52
|
+
process.stdout.write(localOutput.raw);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
51
55
|
process.stdout.write(`${JSON.stringify(applyOutputProjection(localOutput, {
|
|
52
56
|
fields: options.fields,
|
|
53
57
|
preset: options.preset
|
package/src/skill.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { EXIT_USAGE } from "./constants.js";
|
|
7
|
+
import { writeJsonError } from "./output.js";
|
|
8
|
+
|
|
9
|
+
export const SKILL_NAME = "gmc-analysis";
|
|
10
|
+
const SKILL_REFERENCE_FILES = ["recipes.md", "pitfalls.md", "quota.md"];
|
|
11
|
+
const PACKAGED_SKILL_DIR = fileURLToPath(new URL(`../skill/${SKILL_NAME}/`, import.meta.url));
|
|
12
|
+
|
|
13
|
+
export function getPackagedSkillDir() {
|
|
14
|
+
return PACKAGED_SKILL_DIR;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readSkillVersion(skillDir) {
|
|
18
|
+
try {
|
|
19
|
+
const source = readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
|
|
20
|
+
const frontmatter = source.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
const version = frontmatter?.[1].match(/^\s*version:\s*"?([0-9][0-9A-Za-z.\-]*)"?\s*$/m);
|
|
22
|
+
return version?.[1] ?? null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function installTargets(cwd = process.cwd()) {
|
|
29
|
+
return {
|
|
30
|
+
project: path.join(cwd, ".claude", "skills", SKILL_NAME),
|
|
31
|
+
global: path.join(homedir(), ".claude", "skills", SKILL_NAME)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function handleSkillInstall(options) {
|
|
36
|
+
const targets = installTargets();
|
|
37
|
+
const target = options.dir
|
|
38
|
+
? path.resolve(String(options.dir))
|
|
39
|
+
: options.global
|
|
40
|
+
? targets.global
|
|
41
|
+
: targets.project;
|
|
42
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
43
|
+
cpSync(PACKAGED_SKILL_DIR, target, { recursive: true });
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
data: {
|
|
47
|
+
ok: true,
|
|
48
|
+
skill: SKILL_NAME,
|
|
49
|
+
version: readSkillVersion(PACKAGED_SKILL_DIR),
|
|
50
|
+
installedPath: target,
|
|
51
|
+
scope: options.dir ? "custom" : options.global ? "global" : "project",
|
|
52
|
+
next: [
|
|
53
|
+
"Claude Code picks up project skills from .claude/skills automatically.",
|
|
54
|
+
"For Codex or other AGENTS.md-based agents run: gmc skill print --format agents-md >> AGENTS.md"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Self-contained AGENTS.md edition: the SKILL.md body with every reference
|
|
62
|
+
* inlined (AGENTS.md consumers do not receive the skill directory), heading
|
|
63
|
+
* levels demoted so the section nests under an existing document.
|
|
64
|
+
*/
|
|
65
|
+
export function buildAgentsMdSection() {
|
|
66
|
+
const version = readSkillVersion(PACKAGED_SKILL_DIR);
|
|
67
|
+
const body = stripFrontmatter(readFileSync(path.join(PACKAGED_SKILL_DIR, "SKILL.md"), "utf8"));
|
|
68
|
+
const references = SKILL_REFERENCE_FILES.map((file) =>
|
|
69
|
+
readFileSync(path.join(PACKAGED_SKILL_DIR, "references", file), "utf8")
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
`<!-- ${SKILL_NAME} skill v${version} - generated by \`gmc skill print --format agents-md\`; refresh by re-running it after CLI updates -->`,
|
|
74
|
+
demoteHeadings(body.trim(), 1),
|
|
75
|
+
...references.map((content) => demoteHeadings(content.trim(), 2))
|
|
76
|
+
].join("\n\n") + "\n";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function handleSkillPrint(options) {
|
|
80
|
+
const format = String(options.format ?? "agents-md");
|
|
81
|
+
if (format === "agents-md") {
|
|
82
|
+
return { raw: buildAgentsMdSection() };
|
|
83
|
+
}
|
|
84
|
+
if (format === "skill-md") {
|
|
85
|
+
return { raw: readFileSync(path.join(PACKAGED_SKILL_DIR, "SKILL.md"), "utf8") };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeJsonError({
|
|
89
|
+
code: "CLI_USAGE_ERROR",
|
|
90
|
+
message: "--format must be one of: agents-md, skill-md"
|
|
91
|
+
});
|
|
92
|
+
process.exit(EXIT_USAGE);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function handleSkillStatus() {
|
|
96
|
+
const targets = installTargets();
|
|
97
|
+
const packagedVersion = readSkillVersion(PACKAGED_SKILL_DIR);
|
|
98
|
+
const installs = Object.entries(targets).map(([scope, dir]) => {
|
|
99
|
+
const installed = existsSync(path.join(dir, "SKILL.md"));
|
|
100
|
+
const version = installed ? readSkillVersion(dir) : null;
|
|
101
|
+
return {
|
|
102
|
+
scope,
|
|
103
|
+
path: dir,
|
|
104
|
+
installed,
|
|
105
|
+
version,
|
|
106
|
+
...(installed && version !== packagedVersion ? { stale: true } : {})
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
data: {
|
|
112
|
+
skill: SKILL_NAME,
|
|
113
|
+
packaged: {
|
|
114
|
+
version: packagedVersion,
|
|
115
|
+
path: PACKAGED_SKILL_DIR
|
|
116
|
+
},
|
|
117
|
+
installs
|
|
118
|
+
},
|
|
119
|
+
...(installs.some((entry) => entry.stale)
|
|
120
|
+
? { warnings: ["An installed copy is older than the packaged skill. Re-run gmc skill install."] }
|
|
121
|
+
: {})
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stripFrontmatter(source) {
|
|
126
|
+
return source.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function demoteHeadings(markdown, levels) {
|
|
130
|
+
const prefix = "#".repeat(levels);
|
|
131
|
+
return markdown.replace(/^(#{1,4}) /gm, `${prefix}$1 `);
|
|
132
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildAgentsMdSection,
|
|
9
|
+
getPackagedSkillDir,
|
|
10
|
+
handleSkillInstall,
|
|
11
|
+
handleSkillStatus,
|
|
12
|
+
readSkillVersion
|
|
13
|
+
} from "./skill.js";
|
|
14
|
+
|
|
15
|
+
test("packaged skill declares a parseable version", () => {
|
|
16
|
+
const version = readSkillVersion(getPackagedSkillDir());
|
|
17
|
+
assert.match(version ?? "", /^\d+\.\d+\.\d+$/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("agents-md edition is self-contained and demotes headings", () => {
|
|
21
|
+
const section = buildAgentsMdSection();
|
|
22
|
+
|
|
23
|
+
assert.match(section, /^<!-- gmc-analysis skill v\d+\.\d+\.\d+/);
|
|
24
|
+
assert.match(section, /^## GMC Analysis/m);
|
|
25
|
+
// References must be inlined for AGENTS.md consumers without the skill dir.
|
|
26
|
+
assert.match(section, /^### Recipes/m);
|
|
27
|
+
assert.match(section, /^### Pitfalls and pivots/m);
|
|
28
|
+
assert.match(section, /^### Quota etiquette/m);
|
|
29
|
+
// Core field-test learnings must survive into the distributed edition.
|
|
30
|
+
assert.match(section, /skippedReasons/);
|
|
31
|
+
assert.match(section, /appearsInGames/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("skill install copies the packaged skill to a custom directory", () => {
|
|
35
|
+
const targetRoot = mkdtempSync(path.join(tmpdir(), "gmc-skill-install-"));
|
|
36
|
+
const targetDir = path.join(targetRoot, "gmc-analysis");
|
|
37
|
+
try {
|
|
38
|
+
const result = handleSkillInstall({ dir: targetDir });
|
|
39
|
+
|
|
40
|
+
assert.equal(result.data.ok, true);
|
|
41
|
+
assert.equal(result.data.scope, "custom");
|
|
42
|
+
assert.equal(result.data.installedPath, targetDir);
|
|
43
|
+
assert.ok(existsSync(path.join(targetDir, "SKILL.md")));
|
|
44
|
+
assert.ok(existsSync(path.join(targetDir, "references", "pitfalls.md")));
|
|
45
|
+
assert.equal(
|
|
46
|
+
readFileSync(path.join(targetDir, "SKILL.md"), "utf8"),
|
|
47
|
+
readFileSync(path.join(getPackagedSkillDir(), "SKILL.md"), "utf8")
|
|
48
|
+
);
|
|
49
|
+
assert.equal(readSkillVersion(targetDir), result.data.version);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(targetRoot, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("skill status reports packaged version and install slots", () => {
|
|
56
|
+
const result = handleSkillStatus();
|
|
57
|
+
|
|
58
|
+
assert.equal(result.data.skill, "gmc-analysis");
|
|
59
|
+
assert.match(result.data.packaged.version ?? "", /^\d+\.\d+\.\d+$/);
|
|
60
|
+
const scopes = result.data.installs.map((entry) => entry.scope).sort();
|
|
61
|
+
assert.deepEqual(scopes, ["global", "project"]);
|
|
62
|
+
for (const entry of result.data.installs) {
|
|
63
|
+
assert.equal(typeof entry.installed, "boolean");
|
|
64
|
+
}
|
|
65
|
+
});
|