@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.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/THIRD_PARTY_NOTICES.md +1930 -0
- package/bin/cmo-skills.mjs +558 -0
- package/catalog.json +1665 -0
- package/package.json +55 -5
- package/skills/cmo-ab-testing/SKILL.md +106 -0
- package/skills/cmo-ad-creative/SKILL.md +103 -0
- package/skills/cmo-ads-audit/SKILL.md +106 -0
- package/skills/cmo-ads-budget/SKILL.md +117 -0
- package/skills/cmo-ads-competitor-research/SKILL.md +86 -0
- package/skills/cmo-ads-launch/SKILL.md +104 -0
- package/skills/cmo-ads-monitoring/SKILL.md +110 -0
- package/skills/cmo-ads-strategy/SKILL.md +105 -0
- package/skills/cmo-aeo-content/SKILL.md +123 -0
- package/skills/cmo-agent-readiness/SKILL.md +110 -0
- package/skills/cmo-ai-brand-monitoring/SKILL.md +110 -0
- package/skills/cmo-ai-citation-gaps/SKILL.md +104 -0
- package/skills/cmo-ai-prompt-research/SKILL.md +89 -0
- package/skills/cmo-ai-visibility/SKILL.md +118 -0
- package/skills/cmo-ai-visibility-audit/SKILL.md +143 -0
- package/skills/cmo-app-store-optimization/SKILL.md +102 -0
- package/skills/cmo-attribution/SKILL.md +120 -0
- package/skills/cmo-autopilot/SKILL.md +133 -0
- package/skills/cmo-blog-planner/SKILL.md +143 -0
- package/skills/cmo-blog-writing/SKILL.md +182 -0
- package/skills/cmo-brand-naming/SKILL.md +108 -0
- package/skills/cmo-brand-profile/SKILL.md +142 -0
- package/skills/cmo-brand-protection/SKILL.md +108 -0
- package/skills/cmo-campaign-analytics/SKILL.md +128 -0
- package/skills/cmo-checkout-cro/SKILL.md +109 -0
- package/skills/cmo-china-social/SKILL.md +102 -0
- package/skills/cmo-churn-prevention/SKILL.md +116 -0
- package/skills/cmo-client-report/SKILL.md +106 -0
- package/skills/cmo-closed-loop-measurement/SKILL.md +89 -0
- package/skills/cmo-co-marketing/SKILL.md +106 -0
- package/skills/cmo-cold-email/SKILL.md +98 -0
- package/skills/cmo-community/SKILL.md +126 -0
- package/skills/cmo-competitor-pages/SKILL.md +109 -0
- package/skills/cmo-competitor-research/SKILL.md +127 -0
- package/skills/cmo-connect/SKILL.md +134 -0
- package/skills/cmo-content-brief/SKILL.md +107 -0
- package/skills/cmo-content-refresh/SKILL.md +107 -0
- package/skills/cmo-content-repurposing/SKILL.md +164 -0
- package/skills/cmo-content-strategy/SKILL.md +163 -0
- package/skills/cmo-copy-editing/SKILL.md +140 -0
- package/skills/cmo-copywriting/SKILL.md +157 -0
- package/skills/cmo-customer-research/SKILL.md +123 -0
- package/skills/cmo-daily/SKILL.md +135 -0
- package/skills/cmo-directory-submissions/SKILL.md +112 -0
- package/skills/cmo-draft-review/SKILL.md +144 -0
- package/skills/cmo-ecommerce-seo/SKILL.md +109 -0
- package/skills/cmo-email-deliverability/SKILL.md +169 -0
- package/skills/cmo-email-sequences/SKILL.md +107 -0
- package/skills/cmo-employee-advocacy/SKILL.md +85 -0
- package/skills/cmo-events-webinars/SKILL.md +106 -0
- package/skills/cmo-expert-panel/SKILL.md +108 -0
- package/skills/cmo-form-cro/SKILL.md +104 -0
- package/skills/cmo-free-tools/SKILL.md +101 -0
- package/skills/cmo-google-ads/SKILL.md +104 -0
- package/skills/cmo-humanize/SKILL.md +138 -0
- package/skills/cmo-image-generation/SKILL.md +131 -0
- package/skills/cmo-influencer/SKILL.md +108 -0
- package/skills/cmo-international-seo/SKILL.md +110 -0
- package/skills/cmo-keyword-clustering/SKILL.md +101 -0
- package/skills/cmo-keyword-research/SKILL.md +107 -0
- package/skills/cmo-landing-page-audit/SKILL.md +102 -0
- package/skills/cmo-landing-page-builder/SKILL.md +104 -0
- package/skills/cmo-lead-magnets/SKILL.md +117 -0
- package/skills/cmo-link-building/SKILL.md +107 -0
- package/skills/cmo-linkedin/SKILL.md +135 -0
- package/skills/cmo-linkedin-ads/SKILL.md +91 -0
- package/skills/cmo-linkedin-engagement/SKILL.md +95 -0
- package/skills/cmo-linkedin-posts/SKILL.md +109 -0
- package/skills/cmo-linkedin-profile/SKILL.md +103 -0
- package/skills/cmo-linkedin-strategy/SKILL.md +101 -0
- package/skills/cmo-local-seo/SKILL.md +110 -0
- package/skills/cmo-market-sizing/SKILL.md +117 -0
- package/skills/cmo-marketing-audit/SKILL.md +120 -0
- package/skills/cmo-marketing-automation/SKILL.md +98 -0
- package/skills/cmo-marketing-ideas/SKILL.md +101 -0
- package/skills/cmo-marketing-plan/SKILL.md +123 -0
- package/skills/cmo-marketing-psychology/SKILL.md +99 -0
- package/skills/cmo-meta-ads/SKILL.md +110 -0
- package/skills/cmo-newsletter/SKILL.md +118 -0
- package/skills/cmo-offer-design/SKILL.md +108 -0
- package/skills/cmo-onboarding-activation/SKILL.md +106 -0
- package/skills/cmo-open-social/SKILL.md +116 -0
- package/skills/cmo-organic-social/SKILL.md +109 -0
- package/skills/cmo-page-cro/SKILL.md +108 -0
- package/skills/cmo-page-seo/SKILL.md +117 -0
- package/skills/cmo-paywalls/SKILL.md +101 -0
- package/skills/cmo-pinterest/SKILL.md +98 -0
- package/skills/cmo-popups/SKILL.md +111 -0
- package/skills/cmo-positioning/SKILL.md +102 -0
- package/skills/cmo-pricing-strategy/SKILL.md +112 -0
- package/skills/cmo-product-launch/SKILL.md +113 -0
- package/skills/cmo-programmatic-seo/SKILL.md +117 -0
- package/skills/cmo-proposals/SKILL.md +112 -0
- package/skills/cmo-prospecting/SKILL.md +107 -0
- package/skills/cmo-public-relations/SKILL.md +102 -0
- package/skills/cmo-reddit/SKILL.md +86 -0
- package/skills/cmo-referral-program/SKILL.md +102 -0
- package/skills/cmo-revops/SKILL.md +109 -0
- package/skills/cmo-sales-enablement/SKILL.md +102 -0
- package/skills/cmo-schema-markup/SKILL.md +123 -0
- package/skills/cmo-search-intent/SKILL.md +101 -0
- package/skills/cmo-seo-audit/SKILL.md +124 -0
- package/skills/cmo-seo-monitoring/SKILL.md +106 -0
- package/skills/cmo-seo-opportunities/SKILL.md +132 -0
- package/skills/cmo-seo-plan/SKILL.md +115 -0
- package/skills/cmo-short-form-video/SKILL.md +104 -0
- package/skills/cmo-signup-flow/SKILL.md +97 -0
- package/skills/cmo-site-architecture/SKILL.md +111 -0
- package/skills/cmo-sms-whatsapp/SKILL.md +101 -0
- package/skills/cmo-social-analytics/SKILL.md +144 -0
- package/skills/cmo-social-crisis/SKILL.md +107 -0
- package/skills/cmo-social-listening/SKILL.md +100 -0
- package/skills/cmo-social-strategy/SKILL.md +141 -0
- package/skills/cmo-team-access/SKILL.md +141 -0
- package/skills/cmo-technical-seo/SKILL.md +163 -0
- package/skills/cmo-thought-leadership/SKILL.md +111 -0
- package/skills/cmo-tracking-setup/SKILL.md +127 -0
- package/skills/cmo-video-production/SKILL.md +122 -0
- package/skills/cmo-video-repurposing/SKILL.md +105 -0
- package/skills/cmo-wasted-spend/SKILL.md +107 -0
- package/skills/cmo-website-setup/SKILL.md +143 -0
- package/skills/cmo-weekly-report/SKILL.md +144 -0
- package/skills/cmo-x-growth/SKILL.md +104 -0
- 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
|
+
}
|