@edtools/cli 0.3.2 → 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/dist/cli/commands/analyze.d.ts +9 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +146 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +70 -26
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +9 -9
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +458 -71
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +38 -1
- package/dist/integrations/gsc-csv-analyzer.d.ts +5 -0
- package/dist/integrations/gsc-csv-analyzer.d.ts.map +1 -0
- package/dist/integrations/gsc-csv-analyzer.js +110 -0
- package/dist/integrations/gsc-csv-analyzer.js.map +1 -0
- package/dist/templates/blog-post.html.ejs +305 -0
- package/dist/types/analytics.d.ts +37 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/analytics.js +2 -0
- package/dist/types/analytics.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/ui/banner.d.ts +7 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +76 -0
- package/dist/ui/banner.js.map +1 -0
- package/package.json +10 -3
package/dist/cli/index.js
CHANGED
|
@@ -5,17 +5,99 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/cli/index.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
8
|
+
import chalk5 from "chalk";
|
|
9
9
|
|
|
10
10
|
// src/cli/commands/init.ts
|
|
11
11
|
import fs from "fs-extra";
|
|
12
12
|
import path from "path";
|
|
13
|
-
import
|
|
13
|
+
import chalk2 from "chalk";
|
|
14
14
|
import ora from "ora";
|
|
15
15
|
import inquirer from "inquirer";
|
|
16
16
|
import { load as loadCheerio } from "cheerio";
|
|
17
|
+
|
|
18
|
+
// src/ui/banner.ts
|
|
19
|
+
import figlet from "figlet";
|
|
20
|
+
import gradient from "gradient-string";
|
|
21
|
+
import boxen from "boxen";
|
|
22
|
+
import chalk from "chalk";
|
|
23
|
+
function generateBanner() {
|
|
24
|
+
const asciiArt = figlet.textSync("edtools", {
|
|
25
|
+
font: "ANSI Shadow",
|
|
26
|
+
horizontalLayout: "default",
|
|
27
|
+
verticalLayout: "default"
|
|
28
|
+
});
|
|
29
|
+
const coloredAscii = gradient(["#00D9FF", "#00FF9F"])(asciiArt);
|
|
30
|
+
const tagline = chalk.gray("AI-Powered Content Marketing CLI");
|
|
31
|
+
const version = chalk.dim(`v${getVersion()}`);
|
|
32
|
+
const content = `${coloredAscii}
|
|
33
|
+
|
|
34
|
+
${tagline} ${version}`;
|
|
35
|
+
return boxen(content, {
|
|
36
|
+
padding: 1,
|
|
37
|
+
margin: 1,
|
|
38
|
+
borderStyle: "round",
|
|
39
|
+
borderColor: "cyan",
|
|
40
|
+
dimBorder: false
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function showWelcome() {
|
|
44
|
+
console.log(generateBanner());
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log(chalk.bold.cyan(" Quick Start:"));
|
|
47
|
+
console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(` ${chalk.cyan("edtools init")} ${chalk.gray("Initialize your project")}`);
|
|
50
|
+
console.log(` ${chalk.cyan("edtools generate")} ${chalk.gray("Generate SEO-optimized posts")}`);
|
|
51
|
+
console.log(` ${chalk.cyan("edtools analyze")} ${chalk.gray("Analyze Google Search Console CSV")}`);
|
|
52
|
+
console.log("");
|
|
53
|
+
console.log(chalk.gray(" Run ") + chalk.cyan("edtools --help") + chalk.gray(" for all commands"));
|
|
54
|
+
console.log("");
|
|
55
|
+
}
|
|
56
|
+
function getVersion() {
|
|
57
|
+
try {
|
|
58
|
+
return "0.4.0";
|
|
59
|
+
} catch {
|
|
60
|
+
return "dev";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function successBox(message) {
|
|
64
|
+
return boxen(chalk.green("\u2713 ") + message, {
|
|
65
|
+
padding: 1,
|
|
66
|
+
margin: 1,
|
|
67
|
+
borderStyle: "round",
|
|
68
|
+
borderColor: "green"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function errorBox(message) {
|
|
72
|
+
return boxen(chalk.red("\u2717 ") + message, {
|
|
73
|
+
padding: 1,
|
|
74
|
+
margin: 1,
|
|
75
|
+
borderStyle: "round",
|
|
76
|
+
borderColor: "red"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function infoBox(title, content) {
|
|
80
|
+
return boxen(`${chalk.cyan.bold(title)}
|
|
81
|
+
|
|
82
|
+
${content}`, {
|
|
83
|
+
padding: 1,
|
|
84
|
+
margin: 1,
|
|
85
|
+
borderStyle: "round",
|
|
86
|
+
borderColor: "cyan"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function warningBox(message) {
|
|
90
|
+
return boxen(chalk.yellow("\u26A0 ") + message, {
|
|
91
|
+
padding: 1,
|
|
92
|
+
margin: 1,
|
|
93
|
+
borderStyle: "round",
|
|
94
|
+
borderColor: "yellow"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/cli/commands/init.ts
|
|
17
99
|
async function initCommand(options) {
|
|
18
|
-
console.log(
|
|
100
|
+
console.log(chalk2.cyan.bold("\n\u{1F680} Initializing Edtools...\n"));
|
|
19
101
|
const projectPath = path.resolve(options.path);
|
|
20
102
|
const configPath = path.join(projectPath, "edtools.config.js");
|
|
21
103
|
if (await fs.pathExists(configPath)) {
|
|
@@ -28,7 +110,7 @@ async function initCommand(options) {
|
|
|
28
110
|
}
|
|
29
111
|
]);
|
|
30
112
|
if (!overwrite) {
|
|
31
|
-
console.log(
|
|
113
|
+
console.log(chalk2.yellow("Cancelled."));
|
|
32
114
|
return;
|
|
33
115
|
}
|
|
34
116
|
}
|
|
@@ -62,7 +144,7 @@ async function initCommand(options) {
|
|
|
62
144
|
} catch (error) {
|
|
63
145
|
spinner.fail("Failed to analyze landing page");
|
|
64
146
|
}
|
|
65
|
-
console.log(
|
|
147
|
+
console.log(chalk2.cyan("\n\u{1F4DD} Please provide some information about your product:\n"));
|
|
66
148
|
const answers = await inquirer.prompt([
|
|
67
149
|
{
|
|
68
150
|
type: "input",
|
|
@@ -133,7 +215,7 @@ async function initCommand(options) {
|
|
|
133
215
|
default: "anthropic"
|
|
134
216
|
}
|
|
135
217
|
]);
|
|
136
|
-
console.log(
|
|
218
|
+
console.log(chalk2.cyan("\n\u{1F511} API Key Setup:\n"));
|
|
137
219
|
const apiKeyAnswer = await inquirer.prompt([
|
|
138
220
|
{
|
|
139
221
|
type: "password",
|
|
@@ -192,17 +274,21 @@ async function initCommand(options) {
|
|
|
192
274
|
await fs.writeFile(edtoolsConfigPath, JSON.stringify(edtoolsConfig, null, 2), "utf-8");
|
|
193
275
|
const gitignorePath = path.join(edtoolsDir, ".gitignore");
|
|
194
276
|
await fs.writeFile(gitignorePath, "*\n!.gitignore\n", "utf-8");
|
|
195
|
-
console.log(
|
|
196
|
-
console.log(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log(successBox("Configuration created successfully!"));
|
|
279
|
+
const filesCreated = `${chalk2.cyan("Files created:")}
|
|
280
|
+
|
|
281
|
+
${chalk2.white("edtools.config.js")}
|
|
282
|
+
${chalk2.gray("Product configuration and settings")}
|
|
283
|
+
|
|
284
|
+
${chalk2.white(".edtools/config.json")}
|
|
285
|
+
${chalk2.gray("API key stored securely (gitignored)")}`;
|
|
286
|
+
console.log(infoBox("Setup Complete", filesCreated));
|
|
287
|
+
console.log(chalk2.cyan.bold("Next steps:"));
|
|
288
|
+
console.log(` ${chalk2.cyan("1.")} Review ${chalk2.white("edtools.config.js")}`);
|
|
289
|
+
console.log(` ${chalk2.cyan("2.")} Generate content: ${chalk2.white("edtools generate -n 3")}
|
|
203
290
|
`);
|
|
204
|
-
console.log(
|
|
205
|
-
console.log(chalk.gray(` This folder is gitignored to keep your key safe
|
|
291
|
+
console.log(chalk2.gray(`\u{1F4A1} Your API key is stored locally and gitignored for security
|
|
206
292
|
`));
|
|
207
293
|
}
|
|
208
294
|
|
|
@@ -210,22 +296,23 @@ async function initCommand(options) {
|
|
|
210
296
|
import fs2 from "fs-extra";
|
|
211
297
|
import path2 from "path";
|
|
212
298
|
import { pathToFileURL } from "url";
|
|
213
|
-
import
|
|
299
|
+
import chalk3 from "chalk";
|
|
214
300
|
import ora2 from "ora";
|
|
301
|
+
import Table from "cli-table3";
|
|
215
302
|
async function generateCommand(options) {
|
|
216
|
-
console.log(
|
|
303
|
+
console.log(chalk3.cyan.bold("\n\u{1F4DD} Generating content...\n"));
|
|
217
304
|
const projectPath = process.cwd();
|
|
218
305
|
const configPath = path2.join(projectPath, "edtools.config.js");
|
|
219
306
|
if (!await fs2.pathExists(configPath)) {
|
|
220
|
-
console.log(
|
|
221
|
-
console.log(
|
|
307
|
+
console.log(chalk3.red("\u2717 No edtools.config.js found"));
|
|
308
|
+
console.log(chalk3.yellow(' Run "edtools init" first\n'));
|
|
222
309
|
process.exit(1);
|
|
223
310
|
}
|
|
224
311
|
const configUrl = pathToFileURL(configPath).href;
|
|
225
312
|
const config = await import(configUrl);
|
|
226
313
|
const productInfo = config.default.product;
|
|
227
314
|
if (!productInfo || !productInfo.name) {
|
|
228
|
-
console.log(
|
|
315
|
+
console.log(chalk3.red("\u2717 Invalid configuration in edtools.config.js"));
|
|
229
316
|
process.exit(1);
|
|
230
317
|
}
|
|
231
318
|
const provider = productInfo.preferredProvider || "anthropic";
|
|
@@ -242,44 +329,65 @@ async function generateCommand(options) {
|
|
|
242
329
|
if (provider === "anthropic") {
|
|
243
330
|
apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY || storedApiKey;
|
|
244
331
|
if (!apiKey) {
|
|
245
|
-
console.log(
|
|
246
|
-
console.log(
|
|
247
|
-
console.log(
|
|
248
|
-
console.log(
|
|
332
|
+
console.log(chalk3.red("\u2717 No Anthropic API key found"));
|
|
333
|
+
console.log(chalk3.yellow(' Run "edtools init" to configure your API key'));
|
|
334
|
+
console.log(chalk3.yellow(" Or set ANTHROPIC_API_KEY environment variable"));
|
|
335
|
+
console.log(chalk3.yellow(" Or use --api-key option\n"));
|
|
249
336
|
process.exit(1);
|
|
250
337
|
}
|
|
251
338
|
} else if (provider === "openai") {
|
|
252
339
|
apiKey = options.apiKey || process.env.OPENAI_API_KEY || storedApiKey;
|
|
253
340
|
if (!apiKey) {
|
|
254
|
-
console.log(
|
|
255
|
-
console.log(
|
|
256
|
-
console.log(
|
|
257
|
-
console.log(
|
|
341
|
+
console.log(chalk3.red("\u2717 No OpenAI API key found"));
|
|
342
|
+
console.log(chalk3.yellow(' Run "edtools init" to configure your API key'));
|
|
343
|
+
console.log(chalk3.yellow(" Or set OPENAI_API_KEY environment variable"));
|
|
344
|
+
console.log(chalk3.yellow(" Or use --api-key option\n"));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let topics = options.topics;
|
|
349
|
+
if (options.fromCsv) {
|
|
350
|
+
const opportunitiesPath = path2.join(projectPath, ".edtools", "opportunities.json");
|
|
351
|
+
if (!await fs2.pathExists(opportunitiesPath)) {
|
|
352
|
+
console.log(chalk3.red("\u2717 No CSV analysis found"));
|
|
353
|
+
console.log(chalk3.yellow(' Run "edtools analyze" first to analyze your GSC data\n'));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const oppData = await fs2.readJson(opportunitiesPath);
|
|
358
|
+
topics = oppData.opportunities.slice(0, parseInt(options.posts, 10)).map((opp) => opp.suggestedTitle);
|
|
359
|
+
console.log(chalk3.cyan("\u{1F4CA} Using topics from CSV analysis:\n"));
|
|
360
|
+
topics.forEach((topic, i) => {
|
|
361
|
+
console.log(` ${chalk3.cyan((i + 1).toString() + ".")} ${chalk3.white(topic)}`);
|
|
362
|
+
});
|
|
363
|
+
console.log("");
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.log(chalk3.red("\u2717 Failed to load CSV analysis"));
|
|
258
366
|
process.exit(1);
|
|
259
367
|
}
|
|
260
368
|
}
|
|
261
369
|
const count = parseInt(options.posts, 10);
|
|
262
370
|
if (isNaN(count) || count < 1 || count > 10) {
|
|
263
|
-
console.log(
|
|
371
|
+
console.log(chalk3.red("\u2717 Invalid number of posts (must be 1-10)"));
|
|
264
372
|
process.exit(1);
|
|
265
373
|
}
|
|
266
374
|
if (count > 5) {
|
|
267
|
-
console.log(
|
|
268
|
-
console.log(
|
|
375
|
+
console.log(chalk3.yellow(`\u26A0\uFE0F Generating ${count} posts at once may trigger spam detection`));
|
|
376
|
+
console.log(chalk3.yellow(" Recommended: 3-5 posts per week\n"));
|
|
269
377
|
}
|
|
270
378
|
const outputDir = path2.resolve(projectPath, options.output);
|
|
271
379
|
await fs2.ensureDir(outputDir);
|
|
272
|
-
console.log(
|
|
273
|
-
console.log(` Product: ${
|
|
274
|
-
console.log(` Category: ${
|
|
275
|
-
console.log(` AI Provider: ${
|
|
276
|
-
console.log(` Posts to generate: ${
|
|
277
|
-
console.log(` Output directory: ${
|
|
380
|
+
console.log(chalk3.cyan("Configuration:"));
|
|
381
|
+
console.log(` Product: ${chalk3.white(productInfo.name)}`);
|
|
382
|
+
console.log(` Category: ${chalk3.white(productInfo.category)}`);
|
|
383
|
+
console.log(` AI Provider: ${chalk3.white(provider === "anthropic" ? "Claude (Anthropic)" : "ChatGPT (OpenAI)")}`);
|
|
384
|
+
console.log(` Posts to generate: ${chalk3.white(count)}`);
|
|
385
|
+
console.log(` Output directory: ${chalk3.white(outputDir)}
|
|
278
386
|
`);
|
|
279
387
|
const generator = new ContentGenerator(apiKey, provider);
|
|
280
388
|
const generateConfig = {
|
|
281
389
|
productInfo,
|
|
282
|
-
topics
|
|
390
|
+
topics,
|
|
283
391
|
count,
|
|
284
392
|
outputDir,
|
|
285
393
|
projectPath,
|
|
@@ -293,61 +401,340 @@ async function generateCommand(options) {
|
|
|
293
401
|
try {
|
|
294
402
|
const result = await generator.generate(generateConfig);
|
|
295
403
|
if (result.success && result.posts.length > 0) {
|
|
296
|
-
spinner.succeed("Content generated successfully!");
|
|
297
|
-
console.log(
|
|
404
|
+
spinner.succeed(chalk3.bold("Content generated successfully!"));
|
|
405
|
+
console.log("");
|
|
406
|
+
const table = new Table({
|
|
407
|
+
head: [
|
|
408
|
+
chalk3.cyan.bold("#"),
|
|
409
|
+
chalk3.cyan.bold("Title"),
|
|
410
|
+
chalk3.cyan.bold("SEO Score"),
|
|
411
|
+
chalk3.cyan.bold("Path")
|
|
412
|
+
],
|
|
413
|
+
colWidths: [5, 40, 12, 50],
|
|
414
|
+
wordWrap: true,
|
|
415
|
+
style: {
|
|
416
|
+
head: [],
|
|
417
|
+
border: ["cyan"]
|
|
418
|
+
}
|
|
419
|
+
});
|
|
298
420
|
result.posts.forEach((post, i) => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
421
|
+
const scoreColor = getScoreColorFn(post.seoScore);
|
|
422
|
+
table.push([
|
|
423
|
+
chalk3.gray((i + 1).toString()),
|
|
424
|
+
chalk3.white(post.title),
|
|
425
|
+
scoreColor(`${post.seoScore}/100`),
|
|
426
|
+
chalk3.gray(post.path)
|
|
427
|
+
]);
|
|
302
428
|
});
|
|
429
|
+
console.log(table.toString());
|
|
430
|
+
console.log("");
|
|
431
|
+
console.log(successBox(`Generated ${result.posts.length} SEO-optimized ${result.posts.length === 1 ? "post" : "posts"}!`));
|
|
303
432
|
if (result.warnings && result.warnings.length > 0) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
console.log(` ${warning}`);
|
|
307
|
-
});
|
|
433
|
+
const warningText = result.warnings.join("\n");
|
|
434
|
+
console.log(warningBox(warningText));
|
|
308
435
|
}
|
|
309
|
-
console.log(
|
|
310
|
-
console.log(` 1. Review generated content in ${
|
|
311
|
-
console.log(` 2. Edit posts to add personal experience/expertise`);
|
|
312
|
-
console.log(` 3. Deploy to your website`);
|
|
313
|
-
console.log(
|
|
314
|
-
|
|
436
|
+
console.log(chalk3.cyan.bold("Next steps:"));
|
|
437
|
+
console.log(` ${chalk3.cyan("1.")} Review generated content in ${chalk3.white(outputDir)}`);
|
|
438
|
+
console.log(` ${chalk3.cyan("2.")} Edit posts to add personal experience/expertise`);
|
|
439
|
+
console.log(` ${chalk3.cyan("3.")} Deploy to your website`);
|
|
440
|
+
console.log("");
|
|
441
|
+
console.log(chalk3.yellow(`\u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
|
|
315
442
|
`));
|
|
316
443
|
} else {
|
|
317
|
-
spinner.fail("Failed to generate content");
|
|
444
|
+
spinner.fail(chalk3.bold("Failed to generate content"));
|
|
318
445
|
if (result.errors && result.errors.length > 0) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log(` ${error}`);
|
|
322
|
-
});
|
|
446
|
+
const errorText = result.errors.join("\n");
|
|
447
|
+
console.log(errorBox(errorText));
|
|
323
448
|
}
|
|
324
449
|
process.exit(1);
|
|
325
450
|
}
|
|
326
451
|
} catch (error) {
|
|
327
|
-
spinner.fail("Error during generation");
|
|
328
|
-
console.log(
|
|
329
|
-
${error.message}
|
|
330
|
-
`));
|
|
452
|
+
spinner.fail(chalk3.bold("Error during generation"));
|
|
453
|
+
console.log(errorBox(error.message));
|
|
331
454
|
process.exit(1);
|
|
332
455
|
}
|
|
333
456
|
}
|
|
334
|
-
function
|
|
335
|
-
if (score >= 80) return
|
|
336
|
-
if (score >= 60) return
|
|
337
|
-
return
|
|
457
|
+
function getScoreColorFn(score) {
|
|
458
|
+
if (score >= 80) return chalk3.green;
|
|
459
|
+
if (score >= 60) return chalk3.yellow;
|
|
460
|
+
return chalk3.red;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/cli/commands/analyze.ts
|
|
464
|
+
import fs4 from "fs-extra";
|
|
465
|
+
import path3 from "path";
|
|
466
|
+
import chalk4 from "chalk";
|
|
467
|
+
import ora3 from "ora";
|
|
468
|
+
import Table2 from "cli-table3";
|
|
469
|
+
|
|
470
|
+
// src/integrations/gsc-csv-analyzer.ts
|
|
471
|
+
import fs3 from "fs-extra";
|
|
472
|
+
import { parse } from "csv-parse/sync";
|
|
473
|
+
async function parseGSCCSV(filePath) {
|
|
474
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
475
|
+
const records = parse(content, {
|
|
476
|
+
columns: true,
|
|
477
|
+
skip_empty_lines: true,
|
|
478
|
+
cast: (value, context) => {
|
|
479
|
+
if (context.column === "Clicks" || context.column === "Impressions") {
|
|
480
|
+
return parseInt(value.replace(/,/g, ""), 10);
|
|
481
|
+
}
|
|
482
|
+
if (context.column === "CTR") {
|
|
483
|
+
return parseFloat(value.replace("%", "")) / 100;
|
|
484
|
+
}
|
|
485
|
+
if (context.column === "Position") {
|
|
486
|
+
return parseFloat(value);
|
|
487
|
+
}
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
const data = records.map((record) => ({
|
|
492
|
+
query: record["Top queries"] || record["Query"] || record["query"] || "",
|
|
493
|
+
impressions: record["Impressions"] || record["impressions"] || 0,
|
|
494
|
+
clicks: record["Clicks"] || record["clicks"] || 0,
|
|
495
|
+
ctr: record["CTR"] || record["ctr"] || 0,
|
|
496
|
+
position: record["Position"] || record["position"] || 0,
|
|
497
|
+
url: record["Top pages"] || record["Page"] || record["url"] || void 0
|
|
498
|
+
}));
|
|
499
|
+
return data.filter((d) => d.query && d.impressions > 0);
|
|
500
|
+
}
|
|
501
|
+
function analyzeGSCData(data, config = {}) {
|
|
502
|
+
const {
|
|
503
|
+
minImpressions = 50,
|
|
504
|
+
minPosition = 20,
|
|
505
|
+
maxPosition = 100,
|
|
506
|
+
minPotentialClicks = 10,
|
|
507
|
+
limit = 10
|
|
508
|
+
} = config;
|
|
509
|
+
const totalQueries = data.length;
|
|
510
|
+
const totalImpressions = data.reduce((sum, d) => sum + d.impressions, 0);
|
|
511
|
+
const totalClicks = data.reduce((sum, d) => sum + d.clicks, 0);
|
|
512
|
+
const avgCTR = totalClicks / totalImpressions || 0;
|
|
513
|
+
const avgPosition = data.reduce((sum, d) => sum + d.position, 0) / totalQueries || 0;
|
|
514
|
+
const opportunities = data.filter((d) => d.impressions >= minImpressions && d.position >= minPosition && d.position <= maxPosition).map((d) => {
|
|
515
|
+
const estimatedCTRTop10 = 0.15;
|
|
516
|
+
const potentialClicks = Math.round(d.impressions * estimatedCTRTop10 - d.clicks);
|
|
517
|
+
let priority = "low";
|
|
518
|
+
if (d.impressions > 100 && potentialClicks > 20) {
|
|
519
|
+
priority = "high";
|
|
520
|
+
} else if (d.impressions > 50 && potentialClicks > 10) {
|
|
521
|
+
priority = "medium";
|
|
522
|
+
}
|
|
523
|
+
const suggestedTitle = generateSuggestedTitle(d.query);
|
|
524
|
+
const reason = `${d.impressions} impressions at position ${d.position.toFixed(1)} \u2192 potential +${potentialClicks} clicks`;
|
|
525
|
+
return {
|
|
526
|
+
keyword: d.query,
|
|
527
|
+
impressions: d.impressions,
|
|
528
|
+
clicks: d.clicks,
|
|
529
|
+
ctr: d.ctr,
|
|
530
|
+
position: d.position,
|
|
531
|
+
potentialClicks,
|
|
532
|
+
priority,
|
|
533
|
+
suggestedTitle,
|
|
534
|
+
reason
|
|
535
|
+
};
|
|
536
|
+
}).filter((opp) => opp.potentialClicks >= minPotentialClicks).sort((a, b) => {
|
|
537
|
+
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|
538
|
+
const priorityDiff = priorityOrder[b.priority] - priorityOrder[a.priority];
|
|
539
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
540
|
+
return b.potentialClicks - a.potentialClicks;
|
|
541
|
+
}).slice(0, limit);
|
|
542
|
+
const topKeywords = [...data].sort((a, b) => b.clicks - a.clicks).slice(0, 10);
|
|
543
|
+
const lowCTRPages = data.filter((d) => d.position <= 20 && d.ctr < 0.02).sort((a, b) => a.ctr - b.ctr).slice(0, 10);
|
|
544
|
+
return {
|
|
545
|
+
totalQueries,
|
|
546
|
+
totalImpressions,
|
|
547
|
+
totalClicks,
|
|
548
|
+
avgCTR,
|
|
549
|
+
avgPosition,
|
|
550
|
+
opportunities,
|
|
551
|
+
topKeywords,
|
|
552
|
+
lowCTRPages
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function generateSuggestedTitle(keyword) {
|
|
556
|
+
const words = keyword.split(" ");
|
|
557
|
+
const capitalized = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
558
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
559
|
+
if (!capitalized.includes(currentYear.toString())) {
|
|
560
|
+
return `${capitalized}: Complete Guide ${currentYear}`;
|
|
561
|
+
}
|
|
562
|
+
return `${capitalized}: Complete Guide`;
|
|
563
|
+
}
|
|
564
|
+
async function saveOpportunities(opportunities, outputPath) {
|
|
565
|
+
const data = {
|
|
566
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
567
|
+
count: opportunities.length,
|
|
568
|
+
opportunities
|
|
569
|
+
};
|
|
570
|
+
await fs3.writeJson(outputPath, data, { spaces: 2 });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/cli/commands/analyze.ts
|
|
574
|
+
async function analyzeCommand(options) {
|
|
575
|
+
console.log(chalk4.cyan.bold("\n\u{1F4CA} Analyzing Google Search Console Data...\n"));
|
|
576
|
+
const projectPath = process.cwd();
|
|
577
|
+
let csvPath;
|
|
578
|
+
if (options.file) {
|
|
579
|
+
csvPath = path3.resolve(options.file);
|
|
580
|
+
} else {
|
|
581
|
+
const files = await fs4.readdir(projectPath);
|
|
582
|
+
const csvFiles = files.filter((f) => f.endsWith(".csv") && f.toLowerCase().includes("search"));
|
|
583
|
+
if (csvFiles.length === 0) {
|
|
584
|
+
console.log(chalk4.red("\u2717 No CSV file found"));
|
|
585
|
+
console.log(chalk4.yellow(" Please provide a CSV file using --file option"));
|
|
586
|
+
console.log(chalk4.yellow(" Example: edtools analyze --file search-console-data.csv\n"));
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
if (csvFiles.length > 1) {
|
|
590
|
+
console.log(chalk4.yellow("\u26A0 Multiple CSV files found:"));
|
|
591
|
+
csvFiles.forEach((f) => console.log(` - ${f}`));
|
|
592
|
+
console.log(chalk4.yellow("\nUsing first file. Specify with --file to use a different one.\n"));
|
|
593
|
+
}
|
|
594
|
+
csvPath = path3.join(projectPath, csvFiles[0]);
|
|
595
|
+
}
|
|
596
|
+
if (!await fs4.pathExists(csvPath)) {
|
|
597
|
+
console.log(chalk4.red(`\u2717 File not found: ${csvPath}
|
|
598
|
+
`));
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
const spinner = ora3("Parsing CSV data...").start();
|
|
602
|
+
let data;
|
|
603
|
+
try {
|
|
604
|
+
data = await parseGSCCSV(csvPath);
|
|
605
|
+
spinner.succeed(`Parsed ${chalk4.white(data.length)} keywords from ${chalk4.white(path3.basename(csvPath))}`);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
spinner.fail("Failed to parse CSV");
|
|
608
|
+
console.log(chalk4.red(`
|
|
609
|
+
Error: ${error.message}
|
|
610
|
+
`));
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
const analyzeSpinner = ora3("Analyzing opportunities...").start();
|
|
614
|
+
const config = {
|
|
615
|
+
minImpressions: options.minImpressions ? parseInt(options.minImpressions, 10) : 50,
|
|
616
|
+
minPosition: options.minPosition ? parseInt(options.minPosition, 10) : 20,
|
|
617
|
+
limit: options.limit ? parseInt(options.limit, 10) : 10
|
|
618
|
+
};
|
|
619
|
+
const analysis = analyzeGSCData(data, config);
|
|
620
|
+
analyzeSpinner.succeed("Analysis complete!");
|
|
621
|
+
console.log("");
|
|
622
|
+
const metricsTable = new Table2({
|
|
623
|
+
head: [chalk4.cyan.bold("Metric"), chalk4.cyan.bold("Value")],
|
|
624
|
+
colWidths: [30, 20],
|
|
625
|
+
style: {
|
|
626
|
+
head: [],
|
|
627
|
+
border: ["cyan"]
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
metricsTable.push(
|
|
631
|
+
["Total Keywords", chalk4.white(analysis.totalQueries.toLocaleString())],
|
|
632
|
+
["Total Impressions", chalk4.white(analysis.totalImpressions.toLocaleString())],
|
|
633
|
+
["Total Clicks", chalk4.white(analysis.totalClicks.toLocaleString())],
|
|
634
|
+
["Average CTR", chalk4.white((analysis.avgCTR * 100).toFixed(2) + "%")],
|
|
635
|
+
["Average Position", chalk4.white(analysis.avgPosition.toFixed(1))]
|
|
636
|
+
);
|
|
637
|
+
console.log(chalk4.bold.cyan("\u{1F4C8} Overall Metrics\n"));
|
|
638
|
+
console.log(metricsTable.toString());
|
|
639
|
+
console.log("");
|
|
640
|
+
if (analysis.opportunities.length === 0) {
|
|
641
|
+
console.log(warningBox("No opportunities found with current criteria.\nTry lowering --min-impressions or increasing --min-position."));
|
|
642
|
+
} else {
|
|
643
|
+
console.log(chalk4.bold.cyan(`\u{1F3AF} Top ${analysis.opportunities.length} Content Opportunities
|
|
644
|
+
`));
|
|
645
|
+
const oppTable = new Table2({
|
|
646
|
+
head: [
|
|
647
|
+
chalk4.cyan.bold("#"),
|
|
648
|
+
chalk4.cyan.bold("Keyword"),
|
|
649
|
+
chalk4.cyan.bold("Impressions"),
|
|
650
|
+
chalk4.cyan.bold("Position"),
|
|
651
|
+
chalk4.cyan.bold("Potential"),
|
|
652
|
+
chalk4.cyan.bold("Priority")
|
|
653
|
+
],
|
|
654
|
+
colWidths: [5, 40, 14, 12, 12, 12],
|
|
655
|
+
wordWrap: true,
|
|
656
|
+
style: {
|
|
657
|
+
head: [],
|
|
658
|
+
border: ["cyan"]
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
analysis.opportunities.forEach((opp, i) => {
|
|
662
|
+
const priorityColor = opp.priority === "high" ? chalk4.green : opp.priority === "medium" ? chalk4.yellow : chalk4.gray;
|
|
663
|
+
oppTable.push([
|
|
664
|
+
chalk4.gray((i + 1).toString()),
|
|
665
|
+
chalk4.white(opp.keyword),
|
|
666
|
+
chalk4.cyan(opp.impressions.toLocaleString()),
|
|
667
|
+
chalk4.yellow(opp.position.toFixed(1)),
|
|
668
|
+
chalk4.green(`+${opp.potentialClicks}`),
|
|
669
|
+
priorityColor(opp.priority.toUpperCase())
|
|
670
|
+
]);
|
|
671
|
+
});
|
|
672
|
+
console.log(oppTable.toString());
|
|
673
|
+
console.log("");
|
|
674
|
+
console.log(chalk4.bold.cyan("\u{1F4A1} Suggested Blog Post Titles\n"));
|
|
675
|
+
analysis.opportunities.slice(0, 5).forEach((opp, i) => {
|
|
676
|
+
console.log(` ${chalk4.cyan((i + 1).toString() + ".")} ${chalk4.white(opp.suggestedTitle)}`);
|
|
677
|
+
console.log(` ${chalk4.gray(opp.reason)}
|
|
678
|
+
`);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
if (analysis.topKeywords.length > 0) {
|
|
682
|
+
console.log(chalk4.bold.cyan("\u2B50 Top Performing Keywords (by clicks)\n"));
|
|
683
|
+
const topTable = new Table2({
|
|
684
|
+
head: [
|
|
685
|
+
chalk4.cyan.bold("#"),
|
|
686
|
+
chalk4.cyan.bold("Keyword"),
|
|
687
|
+
chalk4.cyan.bold("Clicks"),
|
|
688
|
+
chalk4.cyan.bold("Impressions"),
|
|
689
|
+
chalk4.cyan.bold("CTR")
|
|
690
|
+
],
|
|
691
|
+
colWidths: [5, 40, 12, 14, 12],
|
|
692
|
+
wordWrap: true,
|
|
693
|
+
style: {
|
|
694
|
+
head: [],
|
|
695
|
+
border: ["cyan"]
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
analysis.topKeywords.slice(0, 5).forEach((kw, i) => {
|
|
699
|
+
topTable.push([
|
|
700
|
+
chalk4.gray((i + 1).toString()),
|
|
701
|
+
chalk4.white(kw.query),
|
|
702
|
+
chalk4.green(kw.clicks.toString()),
|
|
703
|
+
chalk4.cyan(kw.impressions.toLocaleString()),
|
|
704
|
+
chalk4.yellow((kw.ctr * 100).toFixed(2) + "%")
|
|
705
|
+
]);
|
|
706
|
+
});
|
|
707
|
+
console.log(topTable.toString());
|
|
708
|
+
console.log("");
|
|
709
|
+
}
|
|
710
|
+
const edtoolsDir = path3.join(projectPath, ".edtools");
|
|
711
|
+
await fs4.ensureDir(edtoolsDir);
|
|
712
|
+
const opportunitiesPath = path3.join(edtoolsDir, "opportunities.json");
|
|
713
|
+
await saveOpportunities(analysis.opportunities, opportunitiesPath);
|
|
714
|
+
console.log(successBox(`Analysis saved to ${chalk4.white(".edtools/opportunities.json")}`));
|
|
715
|
+
console.log(chalk4.cyan.bold("Next steps:"));
|
|
716
|
+
console.log(` ${chalk4.cyan("1.")} Generate content: ${chalk4.white("edtools generate --from-csv")}`);
|
|
717
|
+
console.log(` ${chalk4.cyan("2.")} Or specify topics: ${chalk4.white('edtools generate -t "topic 1" "topic 2"')}
|
|
718
|
+
`);
|
|
719
|
+
console.log(chalk4.gray("\u{1F4A1} Use --from-csv to automatically generate posts for top opportunities\n"));
|
|
338
720
|
}
|
|
339
721
|
|
|
340
722
|
// src/cli/index.ts
|
|
341
723
|
var program = new Command();
|
|
342
|
-
program.name("edtools").description("
|
|
724
|
+
program.name("edtools").description("AI-Powered Content Marketing CLI").version("0.4.0");
|
|
343
725
|
program.command("init").description("Initialize edtools in your project").option("-p, --path <path>", "Project path", process.cwd()).action(initCommand);
|
|
344
|
-
program.command("generate").description("Generate blog posts").option("-n, --posts <number>", "Number of posts to generate", "3").option("-t, --topics <topics...>", "Specific topics to write about").option("-o, --output <dir>", "Output directory", "./blog").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env var)").action(generateCommand);
|
|
726
|
+
program.command("generate").description("Generate blog posts").option("-n, --posts <number>", "Number of posts to generate", "3").option("-t, --topics <topics...>", "Specific topics to write about").option("-o, --output <dir>", "Output directory", "./blog").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env var)").option("--from-csv", "Generate from CSV analysis opportunities").action(generateCommand);
|
|
727
|
+
program.command("analyze").description("Analyze Google Search Console CSV data").option("-f, --file <path>", "Path to CSV file (auto-detects if not provided)").option("--min-impressions <number>", "Minimum impressions for opportunities", "50").option("--min-position <number>", "Minimum position for opportunities", "20").option("--limit <number>", "Number of opportunities to show", "10").action(analyzeCommand);
|
|
345
728
|
program.command("config").description("View or set configuration").option("--set-api-key <key>", "Set Anthropic API key").action(async (options) => {
|
|
346
729
|
if (options.setApiKey) {
|
|
347
|
-
console.log(
|
|
730
|
+
console.log(chalk5.green("\u2713 API key saved"));
|
|
348
731
|
} else {
|
|
349
|
-
console.log(
|
|
732
|
+
console.log(chalk5.cyan("Configuration:"));
|
|
350
733
|
console.log(` API Key: ${process.env.ANTHROPIC_API_KEY ? "[set]" : "[not set]"}`);
|
|
351
734
|
}
|
|
352
735
|
});
|
|
736
|
+
if (process.argv.length === 2) {
|
|
737
|
+
showWelcome();
|
|
738
|
+
process.exit(0);
|
|
739
|
+
}
|
|
353
740
|
program.parse();
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,kCAAkC,CAAC;KAC/C,OAAO,CAAC,OAAO,CAAC,CAAC;AAGpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,oCAAoC,CAAC;KACjD,MAAM,CAAC,mBAAmB,EAAE,cAAc,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;KAC1D,MAAM,CAAC,WAAW,CAAC,CAAC;AAGvB,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,qBAAqB,CAAC;KAClC,MAAM,CAAC,sBAAsB,EAAE,6BAA6B,EAAE,GAAG,CAAC;KAClE,MAAM,CAAC,0BAA0B,EAAE,gCAAgC,CAAC;KACpE,MAAM,CAAC,oBAAoB,EAAE,kBAAkB,EAAE,QAAQ,CAAC;KAC1D,MAAM,CAAC,iBAAiB,EAAE,sDAAsD,CAAC;KACjF,MAAM,CAAC,YAAY,EAAE,0CAA0C,CAAC;KAChE,MAAM,CAAC,eAAe,CAAC,CAAC;AAG3B,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,wCAAwC,CAAC;KACrD,MAAM,CAAC,mBAAmB,EAAE,iDAAiD,CAAC;KAC9E,MAAM,CAAC,4BAA4B,EAAE,uCAAuC,EAAE,IAAI,CAAC;KACnF,MAAM,CAAC,yBAAyB,EAAE,oCAAoC,EAAE,IAAI,CAAC;KAC7E,MAAM,CAAC,kBAAkB,EAAE,iCAAiC,EAAE,IAAI,CAAC;KACnE,MAAM,CAAC,cAAc,CAAC,CAAC;AAG1B,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,2BAA2B,CAAC;KACxC,MAAM,CAAC,qBAAqB,EAAE,uBAAuB,CAAC;KACtD,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QAEtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC;IAC9C,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IACrF,CAAC;AACH,CAAC,CAAC,CAAC;AAGL,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC9B,WAAW,EAAE,CAAC;IACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAGD,OAAO,CAAC,KAAK,EAAE,CAAC"}
|