@witchpot/gmc 0.1.6 → 0.1.7

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,16 @@
2
2
 
3
3
  All notable changes to the Game Market Copilot CLI will be recorded here.
4
4
 
5
+ ## 0.1.7 - 2026-06-12
6
+
7
+ - Bundled the `gmc-analysis` Agent Skill (v0.1.0): analysis recipes, pivot
8
+ heuristics, quota etiquette, and honesty rules for Claude Code / Codex
9
+ agents, seeded from the 2026-06-12 field test.
10
+ - Added `gmc skill install` (project `.claude/skills/` or `--global`),
11
+ `gmc skill print --format agents-md` (self-contained AGENTS.md section
12
+ with references inlined), and `gmc skill status` (packaged vs installed
13
+ version check with stale warnings).
14
+
5
15
  ## 0.1.6 - 2026-06-12
6
16
 
7
17
  - 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.6",
3
+ "version": "0.1.7",
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) {
@@ -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,14 @@
1
+ # gmc-analysis skill changelog
2
+
3
+ ## 0.1.0 - 2026-06-12
4
+
5
+ - Initial release, seeded from the 2026-06-12 field test (roguelike cohort
6
+ complaint analysis over 114 games):
7
+ - core workflow: coverage pre-estimation -> bounded workflow -> pivot ->
8
+ exemplar drill-down;
9
+ - pitfalls P1-P5 including the cross-game pattern aggregation pivot and
10
+ the coverage-vs-topics distinction;
11
+ - full-cohort map-reduce recipe with `--fields` projection and subagent
12
+ chunking;
13
+ - quota etiquette and honesty rules (skippedReasons, locked/preview,
14
+ bounded-evidence caveats).
@@ -0,0 +1,94 @@
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.1.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` and `gmc <command> --help` are the
30
+ source of truth for commands and parameters. Always pass `--json`.
31
+
32
+ ## Core workflow for cohort questions
33
+
34
+ 1. **Size the cohort and its research coverage first** (1-2 requests):
35
+ `gmc games count --source steam <filters> --json`, then again with
36
+ `--coverage full,partial`. Deep analysis exists only for researched
37
+ titles; most of the catalog is `basic` (catalog + snapshot only). This
38
+ tells you the denominator and the fan-out cost before you spend quota.
39
+ 2. **Try the bounded cross-game workflow** that matches the question
40
+ (`gmc reviews patterns`, `gmc marketing patterns`, `gmc cohort ...`) with
41
+ `--coverage full,partial` in the filters.
42
+ 3. **Pivot when the workflow under-delivers** — see the pitfalls reference.
43
+ The most common pivot: per-game `gmc games analysis --sections reviews`
44
+ over the cohort, then cluster the named theme labels yourself.
45
+ 4. **Drill into exemplars**: `gmc games detail`, `gmc games analysis`,
46
+ `gmc games success-report`, `gmc campaigns signals` on 2-5 representative
47
+ titles to add texture to the synthesis.
48
+
49
+ For cohorts beyond one page, and for the full-cohort map-reduce procedure
50
+ (pagination, bounded concurrency, `--fields` projection, subagent chunking),
51
+ follow the recipes reference.
52
+
53
+ ## Hard rules
54
+
55
+ - **Missing data is not absent sentiment.** Report `cohort.usableGames`,
56
+ `cohort.skippedGames`, and `cohort.skippedReasons`. "No review topics
57
+ collected" must never be presented as "players have no complaints".
58
+ - **Locked is locked.** When `meta.entitlements.locked` or `preview: true`
59
+ appears, present what you got as a preview, name the lock key (e.g.
60
+ `success_report.full`), and never imply you read the full content.
61
+ - **Bounded evidence, not market truth.** Carry the CLI's own caveats:
62
+ evidence packs are sampled/returned-page evidence. Always state
63
+ denominators ("87 of 107 analyzed titles") and preserve `warnings`.
64
+ - **Separate API facts from your synthesis.** Quote theme labels and player
65
+ counts as data; label your clustering and conclusions as your analysis.
66
+ - **Pace your quota.** Read `meta.entitlements.quota` (`used`/`limit`)
67
+ before fanning out and keep headroom; on HTTP 429 respect `Retry-After`.
68
+ Details in the quota reference.
69
+ - Preserve `meta.requestId` values for anything you may need to report.
70
+
71
+ ## References
72
+
73
+ - `references/recipes.md` — step-by-step playbooks (cohort complaints,
74
+ full-cohort map-reduce, single-title deep dive, market sizing).
75
+ - `references/pitfalls.md` — known failure modes and the correct pivots.
76
+ - `references/quota.md` — quota model and cost estimation.
77
+
78
+ (In the AGENTS.md edition of this skill the references are inlined below.)
79
+
80
+ ## When recipes fail
81
+
82
+ If a documented recipe fails or you discover a better workaround, record a
83
+ learning entry so the skill can be improved:
84
+
85
+ ```markdown
86
+ - Source: field usage
87
+ - Task: <the analysis question>
88
+ - What happened: <observed behavior, requestIds>
89
+ - Gap or win: <missing knowledge / what worked>
90
+ - Proposed rule: <one-sentence candidate rule>
91
+ ```
92
+
93
+ Suggest the user submits it through the GMC feedback channel (template in
94
+ the gmc user guide).
@@ -0,0 +1,50 @@
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. Don't paginate by brute force
46
+
47
+ List endpoints cap at `limit=100`. For "how many" questions use
48
+ `gmc games count`; for distributions use `gmc games aggregate` — both are
49
+ single-request server aggregates. Reserve pagination loops for when you
50
+ actually need the rows (recipe R2).
@@ -0,0 +1,24 @@
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.
13
+ 2. **Estimate fan-out cost first.** A cohort sweep costs roughly
14
+ `search pages + (1 per game per section call)`. A 114-game cohort with one
15
+ analysis call each ~= 120 requests. Compare against remaining quota and
16
+ keep at least ~20% headroom for drill-downs and retries.
17
+ 3. **Shrink the cohort before shrinking the question.** Use
18
+ `--coverage full,partial` and tighter filters so requests are not spent on
19
+ titles that would be skipped anyway.
20
+ 4. **On HTTP 429 (`QUOTA_EXCEEDED`)**: stop fanning out, respect the
21
+ `Retry-After` header / `error.retryAfterSeconds` (the window resets at UTC
22
+ midnight), and tell the user how much of the analysis completed.
23
+ 5. **Prefer server aggregates** (`count`, `aggregate`) over row pagination —
24
+ they answer sizing questions in one request.
@@ -0,0 +1,88 @@
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
+ ## Output contract for every analysis
84
+
85
+ Always include in your final answer: the cohort definition and size, how many
86
+ games carried the evidence (usable vs skipped, with reasons), which plan/
87
+ entitlement context applied, notable `warnings`, and a clear line between
88
+ 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.6";
11
+ export const CLI_VERSION = "0.1.7";
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
+ });