@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/index.js CHANGED
@@ -5,17 +5,99 @@ import {
5
5
 
6
6
  // src/cli/index.ts
7
7
  import { Command } from "commander";
8
- import chalk3 from "chalk";
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 chalk from "chalk";
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(chalk.cyan.bold("\n\u{1F680} Initializing Edtools...\n"));
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(chalk.yellow("Cancelled."));
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(chalk.cyan("\n\u{1F4DD} Please provide some information about your product:\n"));
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(chalk.cyan("\n\u{1F511} API Key Setup:\n"));
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(chalk.green("\n\u2713 Configuration created successfully!\n"));
196
- console.log(chalk.cyan("Files created:"));
197
- console.log(` - ${chalk.white("edtools.config.js")}`);
198
- console.log(` - ${chalk.white(".edtools/config.json")} (API key stored securely)
199
- `);
200
- console.log(chalk.cyan("Next steps:"));
201
- console.log(` 1. Review ${chalk.white("edtools.config.js")}`);
202
- console.log(` 2. Generate content: ${chalk.white("edtools generate")}
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(chalk.gray(`\u{1F4A1} Your API key is stored locally in .edtools/config.json`));
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 chalk2 from "chalk";
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(chalk2.cyan.bold("\n\u{1F4DD} Generating content...\n"));
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(chalk2.red("\u2717 No edtools.config.js found"));
221
- console.log(chalk2.yellow(' Run "edtools init" first\n'));
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(chalk2.red("\u2717 Invalid configuration in edtools.config.js"));
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(chalk2.red("\u2717 No Anthropic API key found"));
246
- console.log(chalk2.yellow(' Run "edtools init" to configure your API key'));
247
- console.log(chalk2.yellow(" Or set ANTHROPIC_API_KEY environment variable"));
248
- console.log(chalk2.yellow(" Or use --api-key option\n"));
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(chalk2.red("\u2717 No OpenAI API key found"));
255
- console.log(chalk2.yellow(' Run "edtools init" to configure your API key'));
256
- console.log(chalk2.yellow(" Or set OPENAI_API_KEY environment variable"));
257
- console.log(chalk2.yellow(" Or use --api-key option\n"));
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(chalk2.red("\u2717 Invalid number of posts (must be 1-10)"));
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(chalk2.yellow(`\u26A0\uFE0F Generating ${count} posts at once may trigger spam detection`));
268
- console.log(chalk2.yellow(" Recommended: 3-5 posts per week\n"));
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(chalk2.cyan("Configuration:"));
273
- console.log(` Product: ${chalk2.white(productInfo.name)}`);
274
- console.log(` Category: ${chalk2.white(productInfo.category)}`);
275
- console.log(` AI Provider: ${chalk2.white(provider === "anthropic" ? "Claude (Anthropic)" : "ChatGPT (OpenAI)")}`);
276
- console.log(` Posts to generate: ${chalk2.white(count)}`);
277
- console.log(` Output directory: ${chalk2.white(outputDir)}
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: options.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(chalk2.green("\n\u2713 Generated posts:\n"));
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
- console.log(`${i + 1}. ${chalk2.white(post.title)}`);
300
- console.log(` ${chalk2.gray(post.path)}`);
301
- console.log(` ${chalk2.cyan("SEO Score:")} ${getScoreColor(post.seoScore)}${post.seoScore}/100${chalk2.reset()}`);
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
- console.log(chalk2.yellow("\n\u26A0\uFE0F Warnings:"));
305
- result.warnings.forEach((warning) => {
306
- console.log(` ${warning}`);
307
- });
433
+ const warningText = result.warnings.join("\n");
434
+ console.log(warningBox(warningText));
308
435
  }
309
- console.log(chalk2.cyan("\nNext steps:"));
310
- console.log(` 1. Review generated content in ${chalk2.white(outputDir)}`);
311
- console.log(` 2. Edit posts to add personal experience/expertise`);
312
- console.log(` 3. Deploy to your website`);
313
- console.log(chalk2.yellow(`
314
- \u{1F4A1} Tip: Wait 3-7 days before generating more posts to avoid spam detection
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
- console.log(chalk2.red("\nErrors:"));
320
- result.errors.forEach((error) => {
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(chalk2.red(`
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 getScoreColor(score) {
335
- if (score >= 80) return chalk2.green("");
336
- if (score >= 60) return chalk2.yellow("");
337
- return chalk2.red("");
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("Generate SEO-optimized content for LLM discovery").version("0.1.0");
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(chalk3.green("\u2713 API key saved"));
730
+ console.log(chalk5.green("\u2713 API key saved"));
348
731
  } else {
349
- console.log(chalk3.cyan("Configuration:"));
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();
@@ -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;AAEzD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,kDAAkD,CAAC;KAC/D,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,eAAe,CAAC,CAAC;AAG3B,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,OAAO,CAAC,KAAK,EAAE,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"}