@cmo-so/skills 0.0.0-stage → 0.4.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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/THIRD_PARTY_NOTICES.md +1930 -0
  4. package/bin/cmo-skills.mjs +558 -0
  5. package/catalog.json +1665 -0
  6. package/package.json +55 -5
  7. package/skills/cmo-ab-testing/SKILL.md +106 -0
  8. package/skills/cmo-ad-creative/SKILL.md +103 -0
  9. package/skills/cmo-ads-audit/SKILL.md +106 -0
  10. package/skills/cmo-ads-budget/SKILL.md +117 -0
  11. package/skills/cmo-ads-competitor-research/SKILL.md +86 -0
  12. package/skills/cmo-ads-launch/SKILL.md +104 -0
  13. package/skills/cmo-ads-monitoring/SKILL.md +110 -0
  14. package/skills/cmo-ads-strategy/SKILL.md +105 -0
  15. package/skills/cmo-aeo-content/SKILL.md +123 -0
  16. package/skills/cmo-agent-readiness/SKILL.md +110 -0
  17. package/skills/cmo-ai-brand-monitoring/SKILL.md +110 -0
  18. package/skills/cmo-ai-citation-gaps/SKILL.md +104 -0
  19. package/skills/cmo-ai-prompt-research/SKILL.md +89 -0
  20. package/skills/cmo-ai-visibility/SKILL.md +118 -0
  21. package/skills/cmo-ai-visibility-audit/SKILL.md +143 -0
  22. package/skills/cmo-app-store-optimization/SKILL.md +102 -0
  23. package/skills/cmo-attribution/SKILL.md +120 -0
  24. package/skills/cmo-autopilot/SKILL.md +133 -0
  25. package/skills/cmo-blog-planner/SKILL.md +143 -0
  26. package/skills/cmo-blog-writing/SKILL.md +182 -0
  27. package/skills/cmo-brand-naming/SKILL.md +108 -0
  28. package/skills/cmo-brand-profile/SKILL.md +142 -0
  29. package/skills/cmo-brand-protection/SKILL.md +108 -0
  30. package/skills/cmo-campaign-analytics/SKILL.md +128 -0
  31. package/skills/cmo-checkout-cro/SKILL.md +109 -0
  32. package/skills/cmo-china-social/SKILL.md +102 -0
  33. package/skills/cmo-churn-prevention/SKILL.md +116 -0
  34. package/skills/cmo-client-report/SKILL.md +106 -0
  35. package/skills/cmo-closed-loop-measurement/SKILL.md +89 -0
  36. package/skills/cmo-co-marketing/SKILL.md +106 -0
  37. package/skills/cmo-cold-email/SKILL.md +98 -0
  38. package/skills/cmo-community/SKILL.md +126 -0
  39. package/skills/cmo-competitor-pages/SKILL.md +109 -0
  40. package/skills/cmo-competitor-research/SKILL.md +127 -0
  41. package/skills/cmo-connect/SKILL.md +134 -0
  42. package/skills/cmo-content-brief/SKILL.md +107 -0
  43. package/skills/cmo-content-refresh/SKILL.md +107 -0
  44. package/skills/cmo-content-repurposing/SKILL.md +164 -0
  45. package/skills/cmo-content-strategy/SKILL.md +163 -0
  46. package/skills/cmo-copy-editing/SKILL.md +140 -0
  47. package/skills/cmo-copywriting/SKILL.md +157 -0
  48. package/skills/cmo-customer-research/SKILL.md +123 -0
  49. package/skills/cmo-daily/SKILL.md +135 -0
  50. package/skills/cmo-directory-submissions/SKILL.md +112 -0
  51. package/skills/cmo-draft-review/SKILL.md +144 -0
  52. package/skills/cmo-ecommerce-seo/SKILL.md +109 -0
  53. package/skills/cmo-email-deliverability/SKILL.md +169 -0
  54. package/skills/cmo-email-sequences/SKILL.md +107 -0
  55. package/skills/cmo-employee-advocacy/SKILL.md +85 -0
  56. package/skills/cmo-events-webinars/SKILL.md +106 -0
  57. package/skills/cmo-expert-panel/SKILL.md +108 -0
  58. package/skills/cmo-form-cro/SKILL.md +104 -0
  59. package/skills/cmo-free-tools/SKILL.md +101 -0
  60. package/skills/cmo-google-ads/SKILL.md +104 -0
  61. package/skills/cmo-humanize/SKILL.md +138 -0
  62. package/skills/cmo-image-generation/SKILL.md +131 -0
  63. package/skills/cmo-influencer/SKILL.md +108 -0
  64. package/skills/cmo-international-seo/SKILL.md +110 -0
  65. package/skills/cmo-keyword-clustering/SKILL.md +101 -0
  66. package/skills/cmo-keyword-research/SKILL.md +107 -0
  67. package/skills/cmo-landing-page-audit/SKILL.md +102 -0
  68. package/skills/cmo-landing-page-builder/SKILL.md +104 -0
  69. package/skills/cmo-lead-magnets/SKILL.md +117 -0
  70. package/skills/cmo-link-building/SKILL.md +107 -0
  71. package/skills/cmo-linkedin/SKILL.md +135 -0
  72. package/skills/cmo-linkedin-ads/SKILL.md +91 -0
  73. package/skills/cmo-linkedin-engagement/SKILL.md +95 -0
  74. package/skills/cmo-linkedin-posts/SKILL.md +109 -0
  75. package/skills/cmo-linkedin-profile/SKILL.md +103 -0
  76. package/skills/cmo-linkedin-strategy/SKILL.md +101 -0
  77. package/skills/cmo-local-seo/SKILL.md +110 -0
  78. package/skills/cmo-market-sizing/SKILL.md +117 -0
  79. package/skills/cmo-marketing-audit/SKILL.md +120 -0
  80. package/skills/cmo-marketing-automation/SKILL.md +98 -0
  81. package/skills/cmo-marketing-ideas/SKILL.md +101 -0
  82. package/skills/cmo-marketing-plan/SKILL.md +123 -0
  83. package/skills/cmo-marketing-psychology/SKILL.md +99 -0
  84. package/skills/cmo-meta-ads/SKILL.md +110 -0
  85. package/skills/cmo-newsletter/SKILL.md +118 -0
  86. package/skills/cmo-offer-design/SKILL.md +108 -0
  87. package/skills/cmo-onboarding-activation/SKILL.md +106 -0
  88. package/skills/cmo-open-social/SKILL.md +116 -0
  89. package/skills/cmo-organic-social/SKILL.md +109 -0
  90. package/skills/cmo-page-cro/SKILL.md +108 -0
  91. package/skills/cmo-page-seo/SKILL.md +117 -0
  92. package/skills/cmo-paywalls/SKILL.md +101 -0
  93. package/skills/cmo-pinterest/SKILL.md +98 -0
  94. package/skills/cmo-popups/SKILL.md +111 -0
  95. package/skills/cmo-positioning/SKILL.md +102 -0
  96. package/skills/cmo-pricing-strategy/SKILL.md +112 -0
  97. package/skills/cmo-product-launch/SKILL.md +113 -0
  98. package/skills/cmo-programmatic-seo/SKILL.md +117 -0
  99. package/skills/cmo-proposals/SKILL.md +112 -0
  100. package/skills/cmo-prospecting/SKILL.md +107 -0
  101. package/skills/cmo-public-relations/SKILL.md +102 -0
  102. package/skills/cmo-reddit/SKILL.md +86 -0
  103. package/skills/cmo-referral-program/SKILL.md +102 -0
  104. package/skills/cmo-revops/SKILL.md +109 -0
  105. package/skills/cmo-sales-enablement/SKILL.md +102 -0
  106. package/skills/cmo-schema-markup/SKILL.md +123 -0
  107. package/skills/cmo-search-intent/SKILL.md +101 -0
  108. package/skills/cmo-seo-audit/SKILL.md +124 -0
  109. package/skills/cmo-seo-monitoring/SKILL.md +106 -0
  110. package/skills/cmo-seo-opportunities/SKILL.md +132 -0
  111. package/skills/cmo-seo-plan/SKILL.md +115 -0
  112. package/skills/cmo-short-form-video/SKILL.md +104 -0
  113. package/skills/cmo-signup-flow/SKILL.md +97 -0
  114. package/skills/cmo-site-architecture/SKILL.md +111 -0
  115. package/skills/cmo-sms-whatsapp/SKILL.md +101 -0
  116. package/skills/cmo-social-analytics/SKILL.md +144 -0
  117. package/skills/cmo-social-crisis/SKILL.md +107 -0
  118. package/skills/cmo-social-listening/SKILL.md +100 -0
  119. package/skills/cmo-social-strategy/SKILL.md +141 -0
  120. package/skills/cmo-team-access/SKILL.md +141 -0
  121. package/skills/cmo-technical-seo/SKILL.md +163 -0
  122. package/skills/cmo-thought-leadership/SKILL.md +111 -0
  123. package/skills/cmo-tracking-setup/SKILL.md +127 -0
  124. package/skills/cmo-video-production/SKILL.md +122 -0
  125. package/skills/cmo-video-repurposing/SKILL.md +105 -0
  126. package/skills/cmo-wasted-spend/SKILL.md +107 -0
  127. package/skills/cmo-website-setup/SKILL.md +143 -0
  128. package/skills/cmo-weekly-report/SKILL.md +144 -0
  129. package/skills/cmo-x-growth/SKILL.md +104 -0
  130. package/skills/cmo-youtube/SKILL.md +126 -0
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env node
2
+ // Install, update and remove the CMO.so marketing skills for Claude Code, Codex, Pi, Cursor,
3
+ // Gemini CLI and other coding agents, in one project (default) or for the current user.
4
+ // No dependencies.
5
+ import { createHash } from "node:crypto";
6
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join, relative, resolve, sep } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
13
+ const SKILLS_ROOT = join(PKG_ROOT, "skills");
14
+ const MCP_URL = "https://mcp.cmo.so/mcp";
15
+ const ID = /^cmo-[a-z0-9-]+$/;
16
+
17
+ class UsageError extends Error {}
18
+
19
+ // ---------- agents ----------
20
+ // Where each agent reads skills. project: relative to the project; user: under the home folder.
21
+ // `detect` lists folders whose presence means the agent is set up (user scope: under home).
22
+ // Several agents share .agents/skills (the cross-agent folder); a shared folder is written once.
23
+ const HOME = () => homedir();
24
+ const AGENTS = {
25
+ claude: { name: "Claude Code", project: ".claude/skills", user: () => join(process.env.CLAUDE_CONFIG_DIR || join(HOME(), ".claude"), "skills"), detect: [".claude"] },
26
+ codex: { name: "Codex", project: ".agents/skills", user: () => join(HOME(), ".agents", "skills"), detect: [".codex"] },
27
+ pi: { name: "Pi", project: ".agents/skills", user: () => join(HOME(), ".agents", "skills"), detect: [".pi"] },
28
+ cursor: { name: "Cursor", project: ".agents/skills", user: () => join(HOME(), ".cursor", "skills"), detect: [".cursor"] },
29
+ gemini: { name: "Gemini CLI", project: ".agents/skills", user: () => join(HOME(), ".gemini", "skills"), detect: [".gemini"] },
30
+ opencode: { name: "OpenCode", project: ".agents/skills", user: () => join(HOME(), ".config", "opencode", "skills"), detect: [".config/opencode", ".opencode"] },
31
+ copilot: { name: "GitHub Copilot", project: ".agents/skills", user: () => join(HOME(), ".copilot", "skills"), detect: [".copilot"] },
32
+ amp: { name: "Amp", project: ".agents/skills", user: () => join(HOME(), ".config", "agents", "skills"), detect: [".config/amp"] },
33
+ cline: { name: "Cline", project: ".agents/skills", user: () => join(HOME(), ".agents", "skills"), detect: [".cline"] },
34
+ windsurf: { name: "Windsurf", project: ".windsurf/skills", user: () => join(HOME(), ".codeium", "windsurf", "skills"), detect: [".codeium/windsurf", ".windsurf"] },
35
+ goose: { name: "Goose", project: ".goose/skills", user: () => join(HOME(), ".config", "goose", "skills"), detect: [".config/goose", ".goose"] },
36
+ kiro: { name: "Kiro", project: ".kiro/skills", user: () => join(HOME(), ".kiro", "skills"), detect: [".kiro"] },
37
+ qwen: { name: "Qwen Code", project: ".qwen/skills", user: () => join(HOME(), ".qwen", "skills"), detect: [".qwen"] },
38
+ trae: { name: "Trae", project: ".trae/skills", user: () => join(HOME(), ".trae", "skills"), detect: [".trae"] },
39
+ roo: { name: "Roo Code", project: ".roo/skills", user: () => join(HOME(), ".roo", "skills"), detect: [".roo"] },
40
+ kilo: { name: "Kilo Code", project: ".kilocode/skills", user: () => join(HOME(), ".kilocode", "skills"), detect: [".kilocode"] },
41
+ augment: { name: "Augment", project: ".augment/skills", user: () => join(HOME(), ".augment", "skills"), detect: [".augment"] },
42
+ droid: { name: "Factory Droid", project: ".factory/skills", user: () => join(HOME(), ".factory", "skills"), detect: [".factory"] },
43
+ openhands: { name: "OpenHands", project: ".openhands/skills", user: () => join(HOME(), ".openhands", "skills"), detect: [".openhands"] },
44
+ antigravity: { name: "Antigravity", project: ".agent/skills", user: () => join(HOME(), ".gemini", "antigravity", "skills"), detect: [".gemini/antigravity"] },
45
+ };
46
+ const ALIASES = { "claude-code": "claude", "gemini-cli": "gemini", "github-copilot": "copilot", "kilocode": "kilo", "factory": "droid", "roo-code": "roo", "qwen-code": "qwen" };
47
+
48
+ function agentDir(agent, scope, root) {
49
+ return scope === "user" ? resolve(AGENTS[agent].user()) : join(root, AGENTS[agent].project);
50
+ }
51
+
52
+ function detected(scope, root) {
53
+ const base = scope === "user" ? HOME() : root;
54
+ return Object.keys(AGENTS).filter((a) => AGENTS[a].detect.some((d) => existsSync(join(base, d))));
55
+ }
56
+
57
+ function parseAgents(value) {
58
+ return list(value).map((a) => {
59
+ const id = ALIASES[a.toLowerCase()] || a.toLowerCase();
60
+ if (!["auto", "all"].includes(id) && !AGENTS[id]) throw new UsageError(`unknown agent ${a} (see \`cmo-skills agents\`)`);
61
+ return id;
62
+ });
63
+ }
64
+
65
+ // Returns [{ dir, agents: [...] }], one entry per distinct folder.
66
+ function targetsFor(opts, scope, root, previous) {
67
+ let agents = parseAgents(opts["--agent"] || "auto");
68
+ if (agents.includes("all")) agents = Object.keys(AGENTS);
69
+ else if (agents.includes("auto")) {
70
+ if (previous?.targets?.length) return previous.targets;
71
+ agents = detected(scope, root);
72
+ // A project with no agent folders yet: use the agents set up for this user, else Claude Code.
73
+ if (!agents.length && scope === "project") agents = detected("user", root);
74
+ if (!agents.length) agents = ["claude"];
75
+ }
76
+ const byDir = new Map();
77
+ for (const a of agents) {
78
+ const dir = agentDir(a, scope, root);
79
+ if (!byDir.has(dir)) byDir.set(dir, []);
80
+ byDir.get(dir).push(a);
81
+ }
82
+ return [...byDir].map(([dir, as]) => ({ dir, agents: as }));
83
+ }
84
+
85
+ // 0.3.0 records listed agent names only; Codex then lived in ~/.codex/skills (CODEX_HOME).
86
+ function legacyTargets(state, scope, root) {
87
+ if (state.targets) return state.targets;
88
+ return (state.agents || []).map((a) => ({
89
+ dir: a === "codex" && scope === "user" ? join(process.env.CODEX_HOME || join(HOME(), ".codex"), "skills") : agentDir(a, scope, root),
90
+ agents: [a],
91
+ }));
92
+ }
93
+
94
+ function mergeTargets(a = [], b = []) {
95
+ const byDir = new Map();
96
+ for (const t of [...a, ...b]) byDir.set(t.dir, [...new Set([...(byDir.get(t.dir) || []), ...t.agents])]);
97
+ return [...byDir].map(([dir, agents]) => ({ dir, agents }));
98
+ }
99
+
100
+ // ---------- arguments ----------
101
+
102
+ function parse(argv) {
103
+ const flags = new Set(["--global", "-g", "--force", "--dry-run", "--mcp", "--installed", "--json", "--all", "--help", "-h", "--version", "-v"]);
104
+ const valued = new Set(["--scope", "--project", "--agent", "-a", "--category", "--skills"]);
105
+ const opts = { _: [] };
106
+ const add = (k, v) => {
107
+ if (k === "-a") k = "--agent";
108
+ opts[k] = k === "--agent" && opts[k] ? `${opts[k]},${v}` : v;
109
+ };
110
+ for (let i = 0; i < argv.length; i++) {
111
+ const a = argv[i];
112
+ if (a.startsWith("--") && a.includes("=") && valued.has(a.split("=")[0])) {
113
+ const [k, ...v] = a.split("=");
114
+ add(k, v.join("="));
115
+ } else if (valued.has(a)) {
116
+ if (argv[i + 1] === undefined) throw new UsageError(`${a} needs a value`);
117
+ add(a, argv[++i]);
118
+ } else if (flags.has(a)) opts[a === "-g" ? "--global" : a] = true;
119
+ else if (a.startsWith("-")) throw new UsageError(`unknown option ${a}`);
120
+ else opts._.push(a);
121
+ }
122
+ return opts;
123
+ }
124
+
125
+ function list(value) {
126
+ return (value || "").split(",").map((s) => s.trim()).filter(Boolean);
127
+ }
128
+
129
+ function scopeOf(opts) {
130
+ if (opts["--global"] && opts["--scope"] && opts["--scope"] !== "user") throw new UsageError("--global means --scope user");
131
+ const scope = opts["--global"] ? "user" : opts["--scope"] || "project";
132
+ if (!["project", "user"].includes(scope)) throw new UsageError(`unknown scope ${scope} (use project or user)`);
133
+ if (scope === "user" && opts["--project"]) throw new UsageError("--project only applies to --scope project");
134
+ return scope;
135
+ }
136
+
137
+ function projectRoot(opts) {
138
+ return resolve(opts["--project"] || process.cwd());
139
+ }
140
+
141
+ function stateFile(scope, root) {
142
+ return scope === "user"
143
+ ? join(resolve(process.env.CMO_SO_HOME || join(HOME(), ".config", "cmo-so")), "skills-install.json")
144
+ : join(root, ".cmo-so", "skills-install.json");
145
+ }
146
+
147
+ // ---------- catalog and selection ----------
148
+
149
+ function loadCatalog() {
150
+ const path = join(PKG_ROOT, "catalog.json");
151
+ if (!existsSync(path) || !existsSync(SKILLS_ROOT)) {
152
+ throw new UsageError("the bundled skills are missing; build the package with `npm pack` (it runs scripts/bundle.mjs)");
153
+ }
154
+ const catalog = JSON.parse(readFileSync(path, "utf8"));
155
+ for (const s of catalog.skills) if (!ID.test(s.id)) throw new Error(`bad skill id in catalog: ${s.id}`);
156
+ return catalog;
157
+ }
158
+
159
+ function resolveSelection(catalog, selection) {
160
+ const byCategory = new Map();
161
+ for (const s of catalog.skills) {
162
+ for (const key of [s.category, s.plugin]) {
163
+ if (!byCategory.has(key)) byCategory.set(key, []);
164
+ byCategory.get(key).push(s.id);
165
+ }
166
+ }
167
+ const ids = new Set();
168
+ if (selection.all) for (const s of catalog.skills) ids.add(s.id);
169
+ for (const c of selection.categories || []) {
170
+ if (!byCategory.has(c)) throw new UsageError(`unknown category ${c} (see \`cmo-skills list\`)`);
171
+ for (const id of byCategory.get(c)) ids.add(id);
172
+ }
173
+ for (const id of selection.skills || []) {
174
+ if (!catalog.skills.some((s) => s.id === id)) throw new UsageError(`unknown skill ${id} (see \`cmo-skills list\`)`);
175
+ ids.add(id);
176
+ }
177
+ // The core skills (connect, daily, …) are what every other skill hands off to.
178
+ if (ids.size && selection.withCore !== false) for (const id of byCategory.get("core") || []) ids.add(id);
179
+ return [...ids].sort();
180
+ }
181
+
182
+ function selectionFrom(opts) {
183
+ const skills = [...opts._, ...list(opts["--skills"])];
184
+ const categories = list(opts["--category"]);
185
+ if (!skills.length && !categories.length) return { all: true };
186
+ return { categories, skills };
187
+ }
188
+
189
+ function mergeSelection(a = {}, b = {}) {
190
+ if (a.all || b.all) return { all: true };
191
+ return {
192
+ categories: [...new Set([...(a.categories || []), ...(b.categories || [])])].sort(),
193
+ skills: [...new Set([...(a.skills || []), ...(b.skills || [])])].sort(),
194
+ };
195
+ }
196
+
197
+ // ---------- files ----------
198
+
199
+ const sha = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
200
+
201
+ function filesUnder(dir, base = dir) {
202
+ if (!existsSync(dir)) return [];
203
+ const out = [];
204
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
205
+ const p = join(dir, e.name);
206
+ if (e.isDirectory()) out.push(...filesUnder(p, base));
207
+ else if (e.isFile()) out.push(relative(base, p));
208
+ }
209
+ return out.sort();
210
+ }
211
+
212
+ function readJson(path) {
213
+ try {
214
+ return JSON.parse(readFileSync(path, "utf8"));
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ // Sync one skill folder. A file is replaced only if it is new, identical, or unchanged since we
221
+ // installed it (its hash matches the record); local edits are kept unless --force.
222
+ function syncSkill(id, target, managed, opts, log, stats) {
223
+ const source = join(SKILLS_ROOT, id);
224
+ const dry = opts["--dry-run"];
225
+ const fresh = !existsSync(target);
226
+ if (fresh) {
227
+ if (!dry) {
228
+ mkdirSync(dirname(target), { recursive: true });
229
+ const staging = `${target}.cmo-skills-${process.pid}`;
230
+ rmSync(staging, { recursive: true, force: true });
231
+ cpSync(source, staging, { recursive: true });
232
+ renameSync(staging, target);
233
+ }
234
+ log(`installed ${target}`);
235
+ stats.installed++;
236
+ }
237
+ const newManaged = {};
238
+ for (const rel of filesUnder(source)) {
239
+ const src = join(source, rel);
240
+ const dst = join(target, rel);
241
+ const want = sha(src);
242
+ if (fresh) {
243
+ newManaged[dst] = want;
244
+ continue;
245
+ }
246
+ const have = existsSync(dst) ? sha(dst) : null;
247
+ if (have === want) {
248
+ newManaged[dst] = want;
249
+ stats.unchanged++;
250
+ } else if (have === null || managed[dst] === have || opts["--force"]) {
251
+ if (!dry) {
252
+ mkdirSync(dirname(dst), { recursive: true });
253
+ writeFileSync(dst, readFileSync(src));
254
+ }
255
+ log(`${have === null ? "added" : "updated"} ${dst}`);
256
+ newManaged[dst] = want;
257
+ stats.updated++;
258
+ } else {
259
+ log(`kept ${dst} (edited locally; --force replaces it)`);
260
+ newManaged[dst] = managed[dst];
261
+ stats.kept++;
262
+ }
263
+ }
264
+ // Files we installed earlier that the package no longer ships.
265
+ if (!fresh) {
266
+ for (const rel of filesUnder(target)) {
267
+ const dst = join(target, rel);
268
+ if (newManaged[dst] !== undefined || managed[dst] === undefined) continue;
269
+ if (managed[dst] === sha(dst) || opts["--force"]) {
270
+ if (!dry) rmSync(dst);
271
+ log(`deleted ${dst} (no longer in the package)`);
272
+ }
273
+ }
274
+ }
275
+ return newManaged;
276
+ }
277
+
278
+ function removeSkill(target, managed, opts, log) {
279
+ if (!existsSync(target)) return true;
280
+ const edited = filesUnder(target).filter((rel) => {
281
+ const p = join(target, rel);
282
+ return managed[p] === undefined || managed[p] !== sha(p);
283
+ });
284
+ if (edited.length && !opts["--force"]) {
285
+ log(`kept ${target} (${edited.length} file(s) edited or added locally; --force removes it)`);
286
+ return false;
287
+ }
288
+ if (!opts["--dry-run"]) rmSync(target, { recursive: true, force: true });
289
+ log(`removed ${target}`);
290
+ return true;
291
+ }
292
+
293
+ // ---------- MCP ----------
294
+
295
+ function mcpProject(root, opts, log) {
296
+ const path = join(root, ".mcp.json");
297
+ let config = {};
298
+ if (existsSync(path)) {
299
+ config = readJson(path);
300
+ if (!config) {
301
+ log(`skipped ${path} (not valid JSON); add the server by hand: ${MCP_URL}`);
302
+ return;
303
+ }
304
+ }
305
+ config.mcpServers ||= {};
306
+ if (config.mcpServers["cmo-so"]) {
307
+ log(`unchanged ${path} (cmo-so server already set)`);
308
+ return;
309
+ }
310
+ config.mcpServers["cmo-so"] = { type: "http", url: MCP_URL };
311
+ if (!opts["--dry-run"]) writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
312
+ log(`added the cmo-so MCP server to ${path} (Claude Code asks you to approve it, then /mcp → cmo-so → Authenticate)`);
313
+ }
314
+
315
+ function mcpHelp(scope) {
316
+ const user = scope === "user";
317
+ return [
318
+ `Connect the CMO.so MCP server (${MCP_URL}; the core skills need it, sign in with your CMO.so account):`,
319
+ ` Claude Code claude mcp add --scope ${user ? "user" : "project"} --transport http cmo-so ${MCP_URL}`,
320
+ ` Codex codex mcp add cmo-so --url ${MCP_URL} (or [mcp_servers.cmo-so] url = "${MCP_URL}" in ~/.codex/config.toml)`,
321
+ ` Gemini CLI gemini mcp add --transport http ${user ? "--scope user " : ""}cmo-so ${MCP_URL}`,
322
+ ` Cursor Settings → MCP → Add → URL ${MCP_URL}`,
323
+ " Pi and agents that start a command: npx -y @cmo-so/mcp (stdio), after `npx @cmo-so/cli login`",
324
+ ].join("\n");
325
+ }
326
+
327
+ // ---------- commands ----------
328
+
329
+ function install(opts, { update = false } = {}) {
330
+ const catalog = loadCatalog();
331
+ const scope = scopeOf(opts);
332
+ const root = projectRoot(opts);
333
+ const state = stateFile(scope, root);
334
+ const previous = readJson(state);
335
+ if (update && !previous) throw new UsageError(`nothing installed in ${scope === "user" ? "user scope" : root}; run \`cmo-skills install\` first`);
336
+ const previousTargets = previous ? legacyTargets(previous, scope, root) : [];
337
+ const targets = targetsFor(opts, scope, root, previous ? { targets: previousTargets } : null);
338
+ const selection = update ? previous.selection : mergeSelection(previous?.selection, selectionFrom(opts));
339
+ const named = [...opts._, ...list(opts["--skills"])];
340
+ const ids = resolveSelection(catalog, update && named.length ? { skills: named, withCore: false } : selection);
341
+ const managed = previous?.files || {};
342
+ const stats = { installed: 0, updated: 0, unchanged: 0, kept: 0, removed: 0 };
343
+ const log = (line) => console.log(opts["--dry-run"] ? `[dry run] ${line}` : line);
344
+ const files = { ...managed };
345
+
346
+ for (const { dir } of targets) {
347
+ for (const id of ids) Object.assign(files, syncSkill(id, join(dir, id), managed, opts, log, stats));
348
+ // Skills that were installed before but are no longer in the package.
349
+ for (const id of previous?.skills || []) {
350
+ if (catalog.skills.some((s) => s.id === id)) continue;
351
+ const target = join(dir, id);
352
+ if (removeSkill(target, managed, opts, log)) {
353
+ stats.removed++;
354
+ for (const k of Object.keys(files)) if (k.startsWith(target + sep)) delete files[k];
355
+ }
356
+ }
357
+ }
358
+
359
+ const known = (id) => catalog.skills.some((s) => s.id === id);
360
+ const installed = [...new Set([...(previous?.skills || []), ...ids])].filter(known).sort();
361
+ const allTargets = mergeTargets(previousTargets, targets);
362
+ if (opts["--mcp"] && scope === "project") mcpProject(root, opts, log);
363
+ if (!opts["--dry-run"]) {
364
+ mkdirSync(dirname(state), { recursive: true });
365
+ const record = { package: PKG.name, version: PKG.version, scope, ...(scope === "project" ? { project: root } : {}), targets: allTargets, selection, skills: installed, files, [update ? "updated_at" : "installed_at"]: new Date().toISOString() };
366
+ writeFileSync(state, `${JSON.stringify(record, null, 2)}\n`);
367
+ }
368
+ console.log(`\n${update ? "Updated" : "Installed"} ${ids.length} CMO.so skills v${PKG.version} (${scope} scope):`);
369
+ for (const t of targets) console.log(` ${t.dir} (${t.agents.map((a) => AGENTS[a]?.name || a).join(", ")})`);
370
+ console.log(`${stats.installed} new, ${stats.updated} files updated, ${stats.unchanged} unchanged, ${stats.kept} kept${stats.removed ? `, ${stats.removed} retired skills removed` : ""}.`);
371
+ if (stats.kept) console.log("Files you edited were kept. Rerun with --force to replace them.");
372
+ const legacyCodex = previousTargets.find((t) => t.dir.endsWith(join(".codex", "skills")));
373
+ if (legacyCodex) console.log(`Note: Codex now reads ~/.agents/skills. Move to it with \`cmo-skills install --scope user --agent codex\`, then delete ${legacyCodex.dir}/cmo-*.`);
374
+ if (!update && !(opts["--mcp"] && scope === "project")) console.log(`\n${mcpHelp(scope)}`);
375
+ if (!update) console.log(`\nRestart your agent (or start a new session) to load the skills. Try: "Connect to CMO.so" or "What needs me today on my website?"`);
376
+ }
377
+
378
+ function remove(opts) {
379
+ const scope = scopeOf(opts);
380
+ const root = projectRoot(opts);
381
+ const state = stateFile(scope, root);
382
+ const previous = readJson(state);
383
+ if (!previous) throw new UsageError(`nothing installed in ${scope === "user" ? "user scope" : root}`);
384
+ const ids = opts["--all"] ? previous.skills : [...opts._, ...list(opts["--skills"])];
385
+ if (!ids.length) throw new UsageError("name the skills to remove, or pass --all");
386
+ const recorded = legacyTargets(previous, scope, root);
387
+ const targets = opts["--agent"] ? targetsFor(opts, scope, root, null) : recorded;
388
+ const log = (line) => console.log(opts["--dry-run"] ? `[dry run] ${line}` : line);
389
+ const files = { ...previous.files };
390
+ const left = new Set(previous.skills);
391
+ for (const id of ids) {
392
+ if (!ID.test(id)) throw new UsageError(`bad skill id ${id}`);
393
+ let gone = true;
394
+ for (const { dir } of targets) {
395
+ const target = join(dir, id);
396
+ if (removeSkill(target, previous.files, opts, log)) {
397
+ for (const k of Object.keys(files)) if (k.startsWith(target + sep)) delete files[k];
398
+ } else gone = false;
399
+ }
400
+ // Removing from some agents only: the skill stays installed for the others.
401
+ if (gone && targets.length === recorded.length) left.delete(id);
402
+ }
403
+ if (opts["--dry-run"]) return;
404
+ if (!left.size) {
405
+ rmSync(state, { force: true });
406
+ console.log("All CMO.so skills removed.");
407
+ return;
408
+ }
409
+ writeFileSync(state, `${JSON.stringify({ ...previous, targets: recorded, selection: { categories: [], skills: [...left].sort() }, skills: [...left].sort(), files, updated_at: new Date().toISOString() }, null, 2)}\n`);
410
+ console.log(`${left.size} CMO.so skills still installed.`);
411
+ }
412
+
413
+ function listCmd(opts) {
414
+ const catalog = loadCatalog();
415
+ let installed = null;
416
+ if (opts["--installed"]) installed = new Set(readJson(stateFile(scopeOf(opts), projectRoot(opts)))?.skills || []);
417
+ const category = opts["--category"];
418
+ const rows = catalog.skills.filter((s) => (!category || s.category === category || s.plugin === category) && (!installed || installed.has(s.id)));
419
+ if (opts["--json"]) {
420
+ console.log(JSON.stringify(rows, null, 2));
421
+ return;
422
+ }
423
+ for (const c of catalog.categories) {
424
+ const inCat = rows.filter((s) => s.category === c.id);
425
+ if (!inCat.length) continue;
426
+ console.log(`\n${c.name} (--category ${c.id})`);
427
+ for (const s of inCat) console.log(` ${s.id.padEnd(30)} ${s.summary}`);
428
+ }
429
+ console.log(`\n${rows.length} skills. Install: npx @cmo-so/skills install [--category seo,social] [--agent codex,pi] [--scope user]`);
430
+ }
431
+
432
+ function agentsCmd(opts) {
433
+ const root = projectRoot(opts);
434
+ const found = { project: new Set(detected("project", root)), user: new Set(detected("user", root)) };
435
+ if (opts["--json"]) {
436
+ console.log(JSON.stringify(Object.entries(AGENTS).map(([id, a]) => ({ id, name: a.name, project: a.project, user: resolve(a.user()), detected: found.user.has(id) || found.project.has(id) })), null, 2));
437
+ return;
438
+ }
439
+ console.log(`${"--agent".padEnd(12)} ${"Agent".padEnd(15)} ${"Project folder".padEnd(18)} User folder`);
440
+ for (const [id, a] of Object.entries(AGENTS)) {
441
+ const mark = found.project.has(id) || found.user.has(id) ? " *" : "";
442
+ console.log(`${id.padEnd(12)} ${a.name.padEnd(15)} ${a.project.padEnd(18)} ${a.user().replace(HOME(), "~")}${mark}`);
443
+ }
444
+ console.log("\n* set up on this machine or in this project. --agent auto (the default) installs for those, else Claude Code.");
445
+ console.log("Combine agents with commas: --agent codex,pi,claude. Agents that share a folder get one copy.");
446
+ }
447
+
448
+ function doctor(opts) {
449
+ const catalog = loadCatalog();
450
+ const scope = scopeOf(opts);
451
+ const root = projectRoot(opts);
452
+ const state = readJson(stateFile(scope, root));
453
+ let problems = 0;
454
+ const report = (ok, line) => {
455
+ console.log(`${ok ? "PASS" : "FAIL"} ${line}`);
456
+ if (!ok) problems++;
457
+ };
458
+ console.log(`@cmo-so/skills ${PKG.version}, ${catalog.skills.length} skills in the package`);
459
+ if (!state) {
460
+ console.log(`INFO nothing installed in ${scope} scope${scope === "project" ? ` (${root})` : ""}`);
461
+ return;
462
+ }
463
+ report(true, `record ${stateFile(scope, root)} (installed v${state.version})`);
464
+ if (state.version !== PKG.version) console.log(`INFO installed v${state.version}, package v${PKG.version}: run \`npx @cmo-so/skills@latest update${scope === "user" ? " --scope user" : ""}\``);
465
+ for (const { dir, agents } of legacyTargets(state, scope, root)) {
466
+ const missing = state.skills.filter((id) => !existsSync(join(dir, id, "SKILL.md")));
467
+ report(!missing.length, `${agents.map((a) => AGENTS[a]?.name || a).join(", ")}: ${state.skills.length - missing.length}/${state.skills.length} skills in ${dir}${missing.length ? ` (missing: ${missing.join(", ")})` : ""}`);
468
+ const edited = Object.entries(state.files).filter(([p, h]) => p.startsWith(dir + sep) && existsSync(p) && sha(p) !== h).length;
469
+ if (edited) console.log(`INFO ${edited} file(s) in ${dir} edited locally (kept on update)`);
470
+ }
471
+ if (scope === "project") {
472
+ const mcp = readJson(join(root, ".mcp.json"));
473
+ console.log(`INFO MCP server in .mcp.json: ${mcp?.mcpServers?.["cmo-so"] ? "yes" : "no (cmo-skills mcp, or --mcp on install)"}`);
474
+ }
475
+ if (problems) process.exitCode = 1;
476
+ }
477
+
478
+ function mcp(opts) {
479
+ const scope = scopeOf(opts);
480
+ if (scope === "project") mcpProject(projectRoot(opts), opts, (l) => console.log(opts["--dry-run"] ? `[dry run] ${l}` : l));
481
+ console.log(mcpHelp(scope));
482
+ }
483
+
484
+ function help() {
485
+ console.log(`@cmo-so/skills ${PKG.version}: the CMO.so marketing skills for Claude Code, Codex, Pi, Cursor,
486
+ Gemini CLI and other coding agents
487
+
488
+ Usage: npx @cmo-so/skills <command> [options]
489
+
490
+ Commands
491
+ install [SKILL ...] Install skills (all by default; the core skills always come along)
492
+ update Update installed skills to this version; adds new skills in the categories you chose
493
+ remove SKILL ... | --all Remove skills (keeps folders you edited unless --force)
494
+ list Show the skills by category
495
+ agents Show the supported agents and their skill folders
496
+ doctor Check an install
497
+ mcp Connect the CMO.so MCP server (writes .mcp.json in project scope)
498
+
499
+ Options
500
+ --scope project|user project (default): skill folders inside the project, e.g. .claude/skills, .agents/skills
501
+ user: folders in your home, e.g. ~/.claude/skills, ~/.agents/skills, for every project
502
+ -g, --global Same as --scope user
503
+ --project PATH Project folder (default: current folder)
504
+ -a, --agent LIST auto (default), all, or agents: claude, codex, pi, cursor, gemini, opencode, copilot,
505
+ windsurf, amp, cline, goose, kiro, qwen, trae, roo, kilo, augment, droid, openhands, antigravity
506
+ --category LIST Categories or plugins, e.g. seo,social or cmo-so-seo
507
+ --skills LIST Skill ids, e.g. cmo-seo-audit,cmo-x-growth
508
+ --mcp Also add the CMO.so MCP server to the project's .mcp.json (Claude Code)
509
+ --force Replace or remove files you edited
510
+ --dry-run Show what would change, change nothing
511
+ --installed, --json For list and agents
512
+
513
+ Examples
514
+ npx @cmo-so/skills install # this project, for the agents set up here
515
+ npx @cmo-so/skills install -g -a codex,pi # every project, Codex and Pi (~/.agents/skills)
516
+ npx @cmo-so/skills install -a claude,codex --category seo,ai-search --mcp
517
+ npx @cmo-so/skills@latest update -g
518
+
519
+ Also: Claude Code plugins (/plugin marketplace add AI-Marketing-OS-LTD/cmo-so-skills),
520
+ Codex plugins (codex plugin marketplace add AI-Marketing-OS-LTD/cmo-so-skills),
521
+ Pi packages (pi install npm:@cmo-so/skills).`);
522
+ }
523
+
524
+ function main(argv) {
525
+ const [command, ...rest] = argv;
526
+ const opts = parse(rest);
527
+ if (!command || ["help", "--help", "-h"].includes(command) || opts["--help"] || opts["-h"]) return help();
528
+ if (["--version", "-v", "version"].includes(command)) return console.log(PKG.version);
529
+ switch (command) {
530
+ case "install":
531
+ case "add":
532
+ return install(opts);
533
+ case "update":
534
+ case "upgrade":
535
+ return install(opts, { update: true });
536
+ case "remove":
537
+ case "uninstall":
538
+ return remove(opts);
539
+ case "list":
540
+ case "ls":
541
+ return listCmd(opts);
542
+ case "agents":
543
+ return agentsCmd(opts);
544
+ case "doctor":
545
+ return doctor(opts);
546
+ case "mcp":
547
+ return mcp(opts);
548
+ default:
549
+ throw new UsageError(`unknown command ${command}; see \`cmo-skills help\``);
550
+ }
551
+ }
552
+
553
+ try {
554
+ main(process.argv.slice(2));
555
+ } catch (error) {
556
+ console.error(`cmo-skills: ${error.message}`);
557
+ process.exitCode = error instanceof UsageError ? 2 : 1;
558
+ }