@cmo-so/skills 0.0.0-stage → 0.3.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 +59 -2
  3. package/THIRD_PARTY_NOTICES.md +1930 -0
  4. package/bin/cmo-skills.mjs +475 -0
  5. package/catalog.json +1665 -0
  6. package/package.json +45 -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,475 @@
1
+ #!/usr/bin/env node
2
+ // Install, update and remove the CMO.so marketing skills for Claude Code and Codex,
3
+ // in one project (default) or for the current user. No dependencies.
4
+ import { createHash } from "node:crypto";
5
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join, relative, resolve, sep } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
12
+ const SKILLS_ROOT = join(PKG_ROOT, "skills");
13
+ const MCP_URL = "https://mcp.cmo.so/mcp";
14
+ const ID = /^cmo-[a-z0-9-]+$/;
15
+ const AGENTS = ["claude", "codex"];
16
+
17
+ function loadCatalog() {
18
+ const path = join(PKG_ROOT, "catalog.json");
19
+ if (!existsSync(path) || !existsSync(SKILLS_ROOT)) {
20
+ throw new UsageError("the bundled skills are missing; build the package with `npm pack` (it runs scripts/bundle.mjs)");
21
+ }
22
+ const catalog = JSON.parse(readFileSync(path, "utf8"));
23
+ for (const s of catalog.skills) if (!ID.test(s.id)) throw new Error(`bad skill id in catalog: ${s.id}`);
24
+ return catalog;
25
+ }
26
+
27
+ class UsageError extends Error {}
28
+
29
+ // ---------- arguments ----------
30
+
31
+ function parse(argv) {
32
+ const flags = new Set(["--global", "--force", "--dry-run", "--mcp", "--installed", "--json", "--all", "--help", "-h", "--version", "-v"]);
33
+ const valued = new Set(["--scope", "--project", "--agent", "--category", "--skills"]);
34
+ const opts = { _: [] };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ if (a.includes("=") && a.startsWith("--") && valued.has(a.split("=")[0])) {
38
+ const [k, ...v] = a.split("=");
39
+ opts[k] = v.join("=");
40
+ } else if (valued.has(a)) {
41
+ if (argv[i + 1] === undefined) throw new UsageError(`${a} needs a value`);
42
+ opts[a] = argv[++i];
43
+ } else if (flags.has(a)) opts[a] = true;
44
+ else if (a.startsWith("-")) throw new UsageError(`unknown option ${a}`);
45
+ else opts._.push(a);
46
+ }
47
+ return opts;
48
+ }
49
+
50
+ function list(value) {
51
+ return (value || "").split(",").map((s) => s.trim()).filter(Boolean);
52
+ }
53
+
54
+ function scopeOf(opts) {
55
+ if (opts["--global"] && opts["--scope"] && opts["--scope"] !== "user") throw new UsageError("--global means --scope user");
56
+ const scope = opts["--global"] ? "user" : opts["--scope"] || "project";
57
+ if (!["project", "user"].includes(scope)) throw new UsageError(`unknown scope ${scope} (use project or user)`);
58
+ if (scope === "user" && opts["--project"]) throw new UsageError("--project only applies to --scope project");
59
+ return scope;
60
+ }
61
+
62
+ function projectRoot(opts) {
63
+ return resolve(opts["--project"] || process.cwd());
64
+ }
65
+
66
+ function userAgentHome(agent) {
67
+ if (agent === "claude") return resolve(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
68
+ return resolve(process.env.CODEX_HOME || join(homedir(), ".codex"));
69
+ }
70
+
71
+ function stateFile(scope, root) {
72
+ return scope === "user"
73
+ ? join(resolve(process.env.CMO_SO_HOME || join(homedir(), ".config", "cmo-so")), "skills-install.json")
74
+ : join(root, ".cmo-so", "skills-install.json");
75
+ }
76
+
77
+ // Where each agent reads skills from.
78
+ // project: Claude Code <project>/.claude/skills, Codex <project>/.agents/skills
79
+ // user: Claude Code ~/.claude/skills (CLAUDE_CONFIG_DIR), Codex ~/.codex/skills (CODEX_HOME)
80
+ function skillDir(agent, scope, root) {
81
+ if (scope === "user") return join(userAgentHome(agent), "skills");
82
+ return join(root, agent === "claude" ? ".claude" : ".agents", "skills");
83
+ }
84
+
85
+ function agentsFor(opts, scope, root, previous) {
86
+ const requested = opts["--agent"] || "auto";
87
+ if (!["auto", "all", ...AGENTS].includes(requested)) throw new UsageError(`unknown agent ${requested} (use auto, claude, codex or all)`);
88
+ if (requested === "all") return [...AGENTS];
89
+ if (requested !== "auto") return [requested];
90
+ if (previous?.agents?.length) return previous.agents;
91
+ const found = AGENTS.filter((a) => existsSync(scope === "user" ? userAgentHome(a) : join(root, a === "claude" ? ".claude" : ".agents")));
92
+ return found.length ? found : ["claude"];
93
+ }
94
+
95
+ // ---------- selection ----------
96
+
97
+ function resolveSelection(catalog, selection) {
98
+ const byCategory = new Map();
99
+ for (const s of catalog.skills) {
100
+ for (const key of [s.category, s.plugin]) {
101
+ if (!byCategory.has(key)) byCategory.set(key, []);
102
+ byCategory.get(key).push(s.id);
103
+ }
104
+ }
105
+ const ids = new Set();
106
+ if (selection.all) for (const s of catalog.skills) ids.add(s.id);
107
+ for (const c of selection.categories || []) {
108
+ if (!byCategory.has(c)) throw new UsageError(`unknown category ${c} (see \`cmo-skills list\`)`);
109
+ for (const id of byCategory.get(c)) ids.add(id);
110
+ }
111
+ for (const id of selection.skills || []) {
112
+ if (!catalog.skills.some((s) => s.id === id)) throw new UsageError(`unknown skill ${id} (see \`cmo-skills list\`)`);
113
+ ids.add(id);
114
+ }
115
+ // The core skills (connect, daily, …) are what every other skill hands off to.
116
+ if (ids.size && selection.withCore !== false) for (const id of byCategory.get("core") || []) ids.add(id);
117
+ return [...ids].sort();
118
+ }
119
+
120
+ function selectionFrom(opts) {
121
+ const skills = [...opts._, ...list(opts["--skills"])];
122
+ const categories = list(opts["--category"]);
123
+ if (!skills.length && !categories.length) return { all: true };
124
+ return { categories, skills };
125
+ }
126
+
127
+ function mergeSelection(a = {}, b = {}) {
128
+ if (a.all || b.all) return { all: true };
129
+ return {
130
+ categories: [...new Set([...(a.categories || []), ...(b.categories || [])])].sort(),
131
+ skills: [...new Set([...(a.skills || []), ...(b.skills || [])])].sort(),
132
+ };
133
+ }
134
+
135
+ // ---------- files ----------
136
+
137
+ const sha = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
138
+
139
+ function filesUnder(dir, base = dir) {
140
+ if (!existsSync(dir)) return [];
141
+ const out = [];
142
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
143
+ const p = join(dir, e.name);
144
+ if (e.isDirectory()) out.push(...filesUnder(p, base));
145
+ else if (e.isFile()) out.push(relative(base, p));
146
+ }
147
+ return out.sort();
148
+ }
149
+
150
+ function readState(path) {
151
+ try {
152
+ return JSON.parse(readFileSync(path, "utf8"));
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ // Sync one skill folder. A file is replaced only if it is new, identical, or unchanged since we
159
+ // installed it (its hash matches the manifest); local edits are kept unless --force.
160
+ function syncSkill(id, target, managed, opts, log, stats) {
161
+ const source = join(SKILLS_ROOT, id);
162
+ const dry = opts["--dry-run"];
163
+ const fresh = !existsSync(target);
164
+ if (fresh) {
165
+ if (!dry) {
166
+ mkdirSync(dirname(target), { recursive: true });
167
+ const staging = `${target}.cmo-skills-${process.pid}`;
168
+ rmSync(staging, { recursive: true, force: true });
169
+ cpSync(source, staging, { recursive: true });
170
+ renameSync(staging, target);
171
+ }
172
+ log(`installed ${target}`);
173
+ stats.installed++;
174
+ }
175
+ const newManaged = {};
176
+ for (const rel of filesUnder(source)) {
177
+ const src = join(source, rel);
178
+ const dst = join(target, rel);
179
+ const key = join(target, rel);
180
+ const want = sha(src);
181
+ if (fresh) {
182
+ newManaged[key] = want;
183
+ continue;
184
+ }
185
+ const have = existsSync(dst) ? sha(dst) : null;
186
+ if (have === want) {
187
+ newManaged[key] = want;
188
+ stats.unchanged++;
189
+ } else if (have === null || managed[key] === have || opts["--force"]) {
190
+ if (!dry) {
191
+ mkdirSync(dirname(dst), { recursive: true });
192
+ writeFileSync(dst, readFileSync(src));
193
+ }
194
+ log(`${have === null ? "added" : "updated"} ${dst}`);
195
+ newManaged[key] = want;
196
+ stats.updated++;
197
+ } else {
198
+ log(`kept ${dst} (edited locally; --force replaces it)`);
199
+ newManaged[key] = managed[key];
200
+ stats.kept++;
201
+ }
202
+ }
203
+ // Files we installed earlier that the package no longer ships.
204
+ if (!fresh) {
205
+ for (const rel of filesUnder(target)) {
206
+ const dst = join(target, rel);
207
+ if (newManaged[dst] !== undefined || managed[dst] === undefined) continue;
208
+ if (managed[dst] === sha(dst) || opts["--force"]) {
209
+ if (!dry) rmSync(dst);
210
+ log(`deleted ${dst} (no longer in the package)`);
211
+ }
212
+ }
213
+ }
214
+ return newManaged;
215
+ }
216
+
217
+ function removeSkill(target, managed, opts, log) {
218
+ if (!existsSync(target)) return true;
219
+ const edited = filesUnder(target).filter((rel) => {
220
+ const p = join(target, rel);
221
+ return managed[p] === undefined || managed[p] !== sha(p);
222
+ });
223
+ if (edited.length && !opts["--force"]) {
224
+ log(`kept ${target} (${edited.length} file(s) edited or added locally; --force removes it)`);
225
+ return false;
226
+ }
227
+ if (!opts["--dry-run"]) rmSync(target, { recursive: true, force: true });
228
+ log(`removed ${target}`);
229
+ return true;
230
+ }
231
+
232
+ // ---------- MCP ----------
233
+
234
+ function mcpProject(root, opts, log) {
235
+ const path = join(root, ".mcp.json");
236
+ let config = {};
237
+ if (existsSync(path)) {
238
+ try {
239
+ config = JSON.parse(readFileSync(path, "utf8"));
240
+ } catch {
241
+ log(`skipped ${path} (not valid JSON); add the server by hand: ${MCP_URL}`);
242
+ return;
243
+ }
244
+ }
245
+ config.mcpServers ||= {};
246
+ if (config.mcpServers["cmo-so"]) {
247
+ log(`unchanged ${path} (cmo-so server already set)`);
248
+ return;
249
+ }
250
+ config.mcpServers["cmo-so"] = { type: "http", url: MCP_URL };
251
+ if (!opts["--dry-run"]) writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
252
+ log(`added the cmo-so MCP server to ${path} (Claude Code asks you to approve it, then /mcp → cmo-so → Authenticate)`);
253
+ }
254
+
255
+ function mcpHelp(scope) {
256
+ const claudeScope = scope === "user" ? "user" : "project";
257
+ return [
258
+ "Connect the CMO.so MCP server (the core skills need it):",
259
+ ` Claude Code: claude mcp add --scope ${claudeScope} --transport http cmo-so ${MCP_URL}`,
260
+ " then /mcp → cmo-so → Authenticate (sign in to CMO.so in the browser)",
261
+ ` Codex: add to ${scope === "user" ? "~/.codex/config.toml" : ".codex/config.toml"}:`,
262
+ " [mcp_servers.cmo-so]",
263
+ ` url = "${MCP_URL}"`,
264
+ " Others: npx -y @cmo-so/mcp (stdio), after `npx @cmo-so/cli login`",
265
+ ].join("\n");
266
+ }
267
+
268
+ // ---------- commands ----------
269
+
270
+ function install(opts, { update = false } = {}) {
271
+ const catalog = loadCatalog();
272
+ const scope = scopeOf(opts);
273
+ const root = projectRoot(opts);
274
+ const state = stateFile(scope, root);
275
+ const previous = readState(state);
276
+ if (update && !previous) throw new UsageError(`nothing installed in ${scope === "user" ? "user scope" : root}; run \`cmo-skills install\` first`);
277
+ const agents = agentsFor(opts, scope, root, previous);
278
+ const asked = update ? {} : selectionFrom(opts);
279
+ const selection = update ? previous.selection : mergeSelection(previous?.selection, asked);
280
+ const ids = resolveSelection(catalog, update && (opts._.length || opts["--skills"]) ? { skills: [...opts._, ...list(opts["--skills"])], withCore: false } : selection);
281
+ const managed = previous?.files || {};
282
+ const stats = { installed: 0, updated: 0, unchanged: 0, kept: 0, removed: 0 };
283
+ const log = (line) => console.log(opts["--dry-run"] ? `[dry run] ${line}` : line);
284
+ const files = { ...managed };
285
+
286
+ for (const agent of agents) {
287
+ const dir = skillDir(agent, scope, root);
288
+ for (const id of ids) Object.assign(files, syncSkill(id, join(dir, id), managed, opts, log, stats));
289
+ // Skills that were installed before but are no longer in the package.
290
+ for (const id of previous?.skills || []) {
291
+ if (catalog.skills.some((s) => s.id === id)) continue;
292
+ const target = join(dir, id);
293
+ if (removeSkill(target, managed, opts, log)) {
294
+ stats.removed++;
295
+ for (const k of Object.keys(files)) if (k.startsWith(target + sep)) delete files[k];
296
+ }
297
+ }
298
+ }
299
+
300
+ const installed = [...new Set([...(update ? previous.skills : previous?.skills || []), ...ids])].filter((id) => catalog.skills.some((s) => s.id === id)).sort();
301
+ const allAgents = [...new Set([...(previous?.agents || []), ...agents])];
302
+ if (opts["--mcp"] && scope === "project") mcpProject(root, opts, log);
303
+ if (!opts["--dry-run"]) {
304
+ mkdirSync(dirname(state), { recursive: true });
305
+ writeFileSync(state, `${JSON.stringify({ package: PKG.name, version: PKG.version, scope, ...(scope === "project" ? { project: root } : {}), agents: allAgents, selection, skills: installed, files, [update ? "updated_at" : "installed_at"]: new Date().toISOString() }, null, 2)}\n`);
306
+ }
307
+ const where = agents.map((a) => skillDir(a, scope, root)).join(", ");
308
+ console.log(`\n${update ? "Updated" : "Installed"} ${ids.length} CMO.so skills v${PKG.version} (${scope} scope) in ${where}: ${stats.installed} new, ${stats.updated} files updated, ${stats.unchanged} unchanged, ${stats.kept} kept${stats.removed ? `, ${stats.removed} retired skills removed` : ""}.`);
309
+ if (stats.kept) console.log("Files you edited were kept. Rerun with --force to replace them.");
310
+ if (!update && !(opts["--mcp"] && scope === "project")) console.log(`\n${mcpHelp(scope)}`);
311
+ 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?"`);
312
+ }
313
+
314
+ function remove(opts) {
315
+ const scope = scopeOf(opts);
316
+ const root = projectRoot(opts);
317
+ const state = stateFile(scope, root);
318
+ const previous = readState(state);
319
+ if (!previous) throw new UsageError(`nothing installed in ${scope === "user" ? "user scope" : root}`);
320
+ const ids = opts["--all"] ? previous.skills : [...opts._, ...list(opts["--skills"])];
321
+ if (!ids.length) throw new UsageError("name the skills to remove, or pass --all");
322
+ const agents = opts["--agent"] && opts["--agent"] !== "auto" ? agentsFor(opts, scope, root, null) : previous.agents;
323
+ const log = (line) => console.log(opts["--dry-run"] ? `[dry run] ${line}` : line);
324
+ const files = { ...previous.files };
325
+ const left = new Set(previous.skills);
326
+ for (const id of ids) {
327
+ if (!ID.test(id)) throw new UsageError(`bad skill id ${id}`);
328
+ let gone = true;
329
+ for (const agent of agents) {
330
+ const target = join(skillDir(agent, scope, root), id);
331
+ if (removeSkill(target, previous.files, opts, log)) {
332
+ for (const k of Object.keys(files)) if (k.startsWith(target + sep)) delete files[k];
333
+ } else gone = false;
334
+ }
335
+ if (gone) left.delete(id);
336
+ }
337
+ if (opts["--dry-run"]) return;
338
+ if (!left.size) {
339
+ rmSync(state, { force: true });
340
+ console.log("All CMO.so skills removed.");
341
+ return;
342
+ }
343
+ const selection = { categories: [], skills: [...left].sort() };
344
+ writeFileSync(state, `${JSON.stringify({ ...previous, selection, skills: [...left].sort(), files, updated_at: new Date().toISOString() }, null, 2)}\n`);
345
+ console.log(`${left.size} CMO.so skills still installed.`);
346
+ }
347
+
348
+ function listCmd(opts) {
349
+ const catalog = loadCatalog();
350
+ let installed = null;
351
+ if (opts["--installed"]) {
352
+ const scope = scopeOf(opts);
353
+ installed = new Set(readState(stateFile(scope, projectRoot(opts)))?.skills || []);
354
+ }
355
+ const category = opts["--category"];
356
+ const rows = catalog.skills.filter((s) => (!category || s.category === category || s.plugin === category) && (!installed || installed.has(s.id)));
357
+ if (opts["--json"]) {
358
+ console.log(JSON.stringify(rows, null, 2));
359
+ return;
360
+ }
361
+ for (const c of catalog.categories) {
362
+ const inCat = rows.filter((s) => s.category === c.id);
363
+ if (!inCat.length) continue;
364
+ console.log(`\n${c.name} (--category ${c.id})`);
365
+ for (const s of inCat) console.log(` ${s.id.padEnd(30)} ${s.summary}`);
366
+ }
367
+ console.log(`\n${rows.length} skills. Install: npx @cmo-so/skills install [--category seo,social] [--scope user]`);
368
+ }
369
+
370
+ function doctor(opts) {
371
+ const catalog = loadCatalog();
372
+ const scope = scopeOf(opts);
373
+ const root = projectRoot(opts);
374
+ const state = readState(stateFile(scope, root));
375
+ let problems = 0;
376
+ const report = (ok, line) => {
377
+ console.log(`${ok ? "PASS" : "FAIL"} ${line}`);
378
+ if (!ok) problems++;
379
+ };
380
+ console.log(`@cmo-so/skills ${PKG.version}, ${catalog.skills.length} skills in the package`);
381
+ if (!state) {
382
+ console.log(`INFO nothing installed in ${scope} scope${scope === "project" ? ` (${root})` : ""}`);
383
+ return;
384
+ }
385
+ report(true, `manifest ${stateFile(scope, root)} (installed v${state.version})`);
386
+ if (state.version !== PKG.version) console.log(`INFO installed v${state.version}, package v${PKG.version}: run \`cmo-skills update${scope === "user" ? " --scope user" : ""}\``);
387
+ for (const agent of state.agents) {
388
+ const dir = skillDir(agent, scope, root);
389
+ const missing = state.skills.filter((id) => !existsSync(join(dir, id, "SKILL.md")));
390
+ report(!missing.length, `${agent}: ${state.skills.length - missing.length}/${state.skills.length} skills in ${dir}${missing.length ? ` (missing: ${missing.join(", ")})` : ""}`);
391
+ const edited = Object.entries(state.files).filter(([p, h]) => p.startsWith(dir + sep) && existsSync(p) && sha(p) !== h).length;
392
+ if (edited) console.log(`INFO ${agent}: ${edited} file(s) edited locally (kept on update)`);
393
+ }
394
+ if (scope === "project") {
395
+ const mcp = readState(join(root, ".mcp.json"));
396
+ console.log(`INFO MCP server in .mcp.json: ${mcp?.mcpServers?.["cmo-so"] ? "yes" : "no (cmo-skills mcp, or --mcp on install)"}`);
397
+ }
398
+ if (problems) process.exitCode = 1;
399
+ }
400
+
401
+ function mcp(opts) {
402
+ const scope = scopeOf(opts);
403
+ if (scope === "project") mcpProject(projectRoot(opts), opts, (l) => console.log(opts["--dry-run"] ? `[dry run] ${l}` : l));
404
+ console.log(mcpHelp(scope));
405
+ }
406
+
407
+ function help() {
408
+ console.log(`@cmo-so/skills ${PKG.version}: the CMO.so marketing skills for Claude Code and Codex
409
+
410
+ Usage: npx @cmo-so/skills <command> [options]
411
+
412
+ Commands
413
+ install [SKILL ...] Install skills (all by default; the core skills always come along)
414
+ update Update installed skills to this version; adds new skills in the categories you chose
415
+ remove SKILL ... | --all Remove skills (keeps folders you edited unless --force)
416
+ list Show the skills by category
417
+ doctor Check an install
418
+ mcp Connect the CMO.so MCP server (writes .mcp.json in project scope)
419
+
420
+ Options
421
+ --scope project|user project (default): <project>/.claude/skills and <project>/.agents/skills
422
+ user: ~/.claude/skills and ~/.codex/skills, for every project
423
+ --global Same as --scope user
424
+ --project PATH Project folder (default: current folder)
425
+ --agent auto|claude|codex|all Which agent to install for (auto: the ones set up, else Claude Code)
426
+ --category LIST Comma-separated categories or plugins, e.g. seo,social or cmo-so-seo
427
+ --skills LIST Comma-separated skill ids, e.g. cmo-seo-audit,cmo-x-growth
428
+ --mcp Also add the CMO.so MCP server to the project's .mcp.json
429
+ --force Replace or remove files you edited
430
+ --dry-run Show what would change, change nothing
431
+ --installed, --json For list
432
+
433
+ Examples
434
+ npx @cmo-so/skills install # all skills, this project
435
+ npx @cmo-so/skills install --scope user # all skills, every project
436
+ npx @cmo-so/skills install --category seo,ai-search --mcp
437
+ npx @cmo-so/skills install cmo-seo-audit cmo-x-growth --agent codex
438
+ npx @cmo-so/skills@latest update --scope user
439
+
440
+ Prefer Claude Code plugins? /plugin marketplace add AI-Marketing-OS-LTD/cmo-so-skills`);
441
+ }
442
+
443
+ function main(argv) {
444
+ const [command, ...rest] = argv;
445
+ const opts = parse(rest);
446
+ if (!command || command === "help" || command === "--help" || command === "-h" || opts["--help"] || opts["-h"]) return help();
447
+ if (command === "--version" || command === "-v" || command === "version") return console.log(PKG.version);
448
+ switch (command) {
449
+ case "install":
450
+ case "add":
451
+ return install(opts);
452
+ case "update":
453
+ case "upgrade":
454
+ return install(opts, { update: true });
455
+ case "remove":
456
+ case "uninstall":
457
+ return remove(opts);
458
+ case "list":
459
+ case "ls":
460
+ return listCmd(opts);
461
+ case "doctor":
462
+ return doctor(opts);
463
+ case "mcp":
464
+ return mcp(opts);
465
+ default:
466
+ throw new UsageError(`unknown command ${command}; see \`cmo-skills help\``);
467
+ }
468
+ }
469
+
470
+ try {
471
+ main(process.argv.slice(2));
472
+ } catch (error) {
473
+ console.error(`cmo-skills: ${error.message}`);
474
+ process.exitCode = error instanceof UsageError ? 2 : 1;
475
+ }