@xyleapp/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/xyle.mjs CHANGED
@@ -8,7 +8,7 @@ const program = new Command();
8
8
  program
9
9
  .name("xyle")
10
10
  .description("SEO Intelligence Engine CLI")
11
- .version("0.1.0");
11
+ .version("0.3.0");
12
12
 
13
13
  registerCommands(program);
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyleapp/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for the Xyle SEO Intelligence Engine",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.mjs CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  SEO_BASE,
18
18
  } from "./api.mjs";
19
19
  import { getCredentials, clearCredentials, runLoginFlow } from "./auth.mjs";
20
+ import { seedInstructions, getToolNames, promptToolSelection, TOOLS } from "./seed.mjs";
20
21
 
21
22
  /**
22
23
  * Print error message in red to stderr and exit.
@@ -284,4 +285,70 @@ export function registerCommands(program) {
284
285
  console.log("\x1b[33mNot authenticated. Run: xyle login\x1b[0m");
285
286
  }
286
287
  });
288
+
289
+ // --- seed ---
290
+ program
291
+ .command("seed")
292
+ .description("Add xyle agent instructions to your project for AI coding tools")
293
+ .option(
294
+ "--tool <name>",
295
+ `Specific tool: ${getToolNames().join(", ")}`
296
+ )
297
+ .option("--all", "Seed all tools without prompting")
298
+ .option("--dir <path>", "Target directory", process.cwd())
299
+ .option("--list", "List supported tools")
300
+ .action(async (opts) => {
301
+ if (opts.list) {
302
+ console.log("Supported tools:\n");
303
+ for (const [name, tool] of Object.entries(TOOLS)) {
304
+ console.log(` ${name.padEnd(14)} ${tool.label.padEnd(26)} ${tool.path}`);
305
+ }
306
+ return;
307
+ }
308
+
309
+ if (opts.tool && !TOOLS[opts.tool]) {
310
+ process.stderr.write(
311
+ `\x1b[31mUnknown tool: ${opts.tool}\nSupported: ${getToolNames().join(", ")}\x1b[0m\n`
312
+ );
313
+ process.exit(1);
314
+ }
315
+
316
+ let toolNames;
317
+ if (opts.tool) {
318
+ toolNames = [opts.tool];
319
+ } else if (opts.all) {
320
+ toolNames = null; // all
321
+ } else {
322
+ // Interactive selection
323
+ const selected = await promptToolSelection();
324
+ if (!selected.length) {
325
+ console.log("No tools selected.");
326
+ return;
327
+ }
328
+ toolNames = selected;
329
+ }
330
+
331
+ const { created, skipped } = seedInstructions(opts.dir, toolNames);
332
+
333
+ if (created.length) {
334
+ console.log("\x1b[32mCreated:\x1b[0m");
335
+ for (const f of created) {
336
+ console.log(` + ${f}`);
337
+ }
338
+ }
339
+ if (skipped.length) {
340
+ console.log("\x1b[33mSkipped:\x1b[0m");
341
+ for (const f of skipped) {
342
+ console.log(` - ${f}`);
343
+ }
344
+ }
345
+ if (!created.length && !skipped.length) {
346
+ console.log("Nothing to do.");
347
+ }
348
+ if (created.length) {
349
+ console.log(
350
+ `\n\x1b[32mDone!\x1b[0m Your AI coding tools will now know about xyle.`
351
+ );
352
+ }
353
+ });
287
354
  }
package/src/seed.mjs ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Seed agent instruction files for popular AI coding tools.
3
+ * Each tool reads from a specific file path to learn about xyle.
4
+ */
5
+
6
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
7
+ import { join, dirname } from "node:path";
8
+
9
+ const INSTRUCTIONS = `# Xyle — SEO Intelligence Engine
10
+
11
+ You have access to the **xyle** CLI for SEO analysis. Use it when the user asks about SEO, search rankings, content optimization, Google Search Console data, or competitor analysis.
12
+
13
+ ## Quick Reference
14
+
15
+ Run commands via npx (no install needed):
16
+
17
+ \`\`\`bash
18
+ npx @xyleapp/cli <command> [options]
19
+ \`\`\`
20
+
21
+ ## Authentication
22
+
23
+ \`\`\`bash
24
+ npx @xyleapp/cli login # Google OAuth (opens browser)
25
+ npx @xyleapp/cli whoami # Check auth status
26
+ npx @xyleapp/cli logout # Remove credentials
27
+ \`\`\`
28
+
29
+ Credentials are stored in \`~/.config/xyle/credentials.json\`.
30
+
31
+ ## Commands
32
+
33
+ ### status — Check API connectivity
34
+ \`\`\`bash
35
+ npx @xyleapp/cli status [--json]
36
+ \`\`\`
37
+
38
+ ### queries — Top search queries from Google Search Console
39
+ \`\`\`bash
40
+ npx @xyleapp/cli queries --site <domain> [--limit <n>] [--json]
41
+ \`\`\`
42
+ Returns: query, impressions, clicks, ctr, position
43
+
44
+ ### competitors — Competitor pages for a search query
45
+ \`\`\`bash
46
+ npx @xyleapp/cli competitors --query "<query>" [--json]
47
+ \`\`\`
48
+ Returns: position, url, title, word_count
49
+
50
+ ### gaps — Content gaps for a page
51
+ \`\`\`bash
52
+ npx @xyleapp/cli gaps --page <url> [--query "<query>"] [--json]
53
+ \`\`\`
54
+ Returns: query, missing_topics, ctr_issue, position_bucket
55
+
56
+ ### analyze — Score page content against competitors
57
+ \`\`\`bash
58
+ npx @xyleapp/cli analyze --url <url> --content "<text>" [--query "<query>"] [--json]
59
+ \`\`\`
60
+ Returns: score (0-1), summary, missing_topics
61
+
62
+ ### rewrite — AI rewrite suggestions
63
+ \`\`\`bash
64
+ npx @xyleapp/cli rewrite --url <url> [--type title|meta|heading|section] [--query "<query>"] [--json]
65
+ \`\`\`
66
+ Returns: original, suggested, reasoning
67
+
68
+ ### crawl — Extract SEO metadata from a URL
69
+ \`\`\`bash
70
+ npx @xyleapp/cli crawl --url <url> [--json]
71
+ \`\`\`
72
+ Returns: title, meta_desc, word_count, headings
73
+
74
+ ### sync — Sync Google Search Console data
75
+ \`\`\`bash
76
+ npx @xyleapp/cli sync --site <url> [--json]
77
+ \`\`\`
78
+
79
+ ## Environment Variables
80
+
81
+ | Variable | Default | Description |
82
+ |----------|---------|-------------|
83
+ | \`SEO_BASE\` | \`http://localhost:8765\` | API base URL |
84
+ | \`AGENT_API_KEY\` | — | Fallback API key (when not using Google OAuth) |
85
+
86
+ ## Workflows
87
+
88
+ ### Full SEO Audit
89
+ 1. \`xyle status\` — verify connectivity
90
+ 2. \`xyle queries --site <domain> --json\` — get top queries
91
+ 3. \`xyle competitors --query "<top query>" --json\` — analyze competition
92
+ 4. \`xyle gaps --page <url> --json\` — find content gaps
93
+ 5. Summarize findings with recommendations
94
+
95
+ ### Page Optimization
96
+ 1. \`xyle crawl --url <url> --json\` — get current page data
97
+ 2. \`xyle analyze --url <url> --content "<text>" --json\` — score content
98
+ 3. \`xyle rewrite --url <url> --type title --json\` — get title suggestion
99
+ 4. \`xyle rewrite --url <url> --type meta --json\` — get meta suggestion
100
+ 5. Present before/after comparison
101
+
102
+ ## Tips
103
+ - Always use \`--json\` when parsing output programmatically
104
+ - Run \`status\` first to verify the API is reachable
105
+ - Use \`crawl\` before \`analyze\` to get page content automatically
106
+ - Combine \`gaps\` + \`rewrite\` for actionable improvements
107
+ `;
108
+
109
+ /**
110
+ * Tool definitions: name → { file path (relative to project root), description }
111
+ */
112
+ export const TOOLS = {
113
+ "claude-code": {
114
+ path: "CLAUDE.md",
115
+ label: "Claude Code",
116
+ },
117
+ cursor: {
118
+ path: ".cursorrules",
119
+ label: "Cursor",
120
+ },
121
+ vscode: {
122
+ path: ".github/copilot-instructions.md",
123
+ label: "VS Code (GitHub Copilot)",
124
+ },
125
+ windsurf: {
126
+ path: ".windsurfrules",
127
+ label: "Windsurf",
128
+ },
129
+ cline: {
130
+ path: ".clinerules",
131
+ label: "Cline",
132
+ },
133
+ codex: {
134
+ path: "AGENTS.md",
135
+ label: "Codex (OpenAI)",
136
+ },
137
+ augment: {
138
+ path: ".augment-guidelines",
139
+ label: "Augment (Anti Gravity)",
140
+ },
141
+ aider: {
142
+ path: "CONVENTIONS.md",
143
+ label: "Aider",
144
+ },
145
+ };
146
+
147
+ /**
148
+ * Prompt user to select tools interactively.
149
+ * @returns {Promise<string[]>} Selected tool names
150
+ */
151
+ export async function promptToolSelection() {
152
+ const readline = await import("node:readline");
153
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
154
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
155
+
156
+ const entries = Object.entries(TOOLS);
157
+ console.log("\nSelect tools to seed (comma-separated numbers, or 'a' for all):\n");
158
+ entries.forEach(([name, tool], i) => {
159
+ console.log(` ${i + 1}) ${tool.label.padEnd(26)} ${tool.path}`);
160
+ });
161
+ console.log();
162
+
163
+ const answer = await ask("Your selection: ");
164
+ rl.close();
165
+
166
+ if (answer.trim().toLowerCase() === "a") {
167
+ return Object.keys(TOOLS);
168
+ }
169
+
170
+ const nums = answer.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
171
+ const selected = nums
172
+ .filter((n) => n >= 1 && n <= entries.length)
173
+ .map((n) => entries[n - 1][0]);
174
+
175
+ return [...new Set(selected)];
176
+ }
177
+
178
+ /**
179
+ * Write agent instruction files to the target directory.
180
+ * @param {string} targetDir - Absolute path to the project root
181
+ * @param {string[]|null} toolNames - Specific tool names, or null for all
182
+ * @returns {{ created: string[], skipped: string[] }}
183
+ */
184
+ export function seedInstructions(targetDir, toolNames) {
185
+ const tools = toolNames
186
+ ? Object.fromEntries(toolNames.map((n) => [n, TOOLS[n]]))
187
+ : TOOLS;
188
+ const created = [];
189
+ const skipped = [];
190
+
191
+ for (const [name, tool] of Object.entries(tools)) {
192
+ const filePath = join(targetDir, tool.path);
193
+ if (existsSync(filePath)) {
194
+ skipped.push(`${tool.label} (${tool.path}) — already exists`);
195
+ continue;
196
+ }
197
+ const dir = dirname(filePath);
198
+ if (!existsSync(dir)) {
199
+ mkdirSync(dir, { recursive: true });
200
+ }
201
+ writeFileSync(filePath, INSTRUCTIONS, "utf-8");
202
+ created.push(`${tool.label} (${tool.path})`);
203
+ }
204
+
205
+ return { created, skipped };
206
+ }
207
+
208
+ /**
209
+ * Get the list of supported tool names.
210
+ */
211
+ export function getToolNames() {
212
+ return Object.keys(TOOLS);
213
+ }