@xyleapp/cli 0.5.0 → 0.7.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.5.0");
11
+ .version("0.7.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.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for the Xyle SEO Intelligence Engine",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.mjs CHANGED
@@ -113,4 +113,8 @@ export function getAuthUrl(cliPort) {
113
113
  });
114
114
  }
115
115
 
116
+ export function getInstructions(tool) {
117
+ return request("GET", "/seed/instructions", { params: { tool }, timeout: 10000 });
118
+ }
119
+
116
120
  export { SEO_BASE };
package/src/commands.mjs CHANGED
@@ -4,6 +4,9 @@
4
4
  */
5
5
 
6
6
  import { createRequire } from "node:module";
7
+ import { existsSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { execSync } from "node:child_process";
7
10
  import { printJson, printTable } from "./formatting.mjs";
8
11
  import {
9
12
  checkHealth,
@@ -135,12 +138,24 @@ export function registerCommands(program) {
135
138
  } else {
136
139
  const score = data.score || 0;
137
140
  const color = score >= 0.7 ? "\x1b[32m" : "\x1b[33m";
138
- console.log(`${color}Score: ${Math.round(score * 100)}%\x1b[0m`);
141
+ console.log(`${color}SEO Score: ${Math.round(score * 100)}%\x1b[0m`);
139
142
  console.log(`Summary: ${data.summary || ""}`);
140
143
  const missing = data.missing_topics || [];
141
144
  if (missing.length) {
142
145
  console.log(`Missing topics: ${missing.join(", ")}`);
143
146
  }
147
+ // AEO Score + Recommendations
148
+ if (data.aeo_score != null) {
149
+ const aeoColor = data.aeo_score >= 0.7 ? "\x1b[32m" : "\x1b[33m";
150
+ console.log(`${aeoColor}AEO Score: ${Math.round(data.aeo_score * 100)}%\x1b[0m`);
151
+ }
152
+ const recs = data.aeo_recommendations || [];
153
+ if (recs.length) {
154
+ console.log("AEO Recommendations:");
155
+ for (const rec of recs) {
156
+ console.log(` \x1b[36m\u2192\x1b[0m ${rec}`);
157
+ }
158
+ }
144
159
  }
145
160
  } catch (e) {
146
161
  handleError(e);
@@ -194,6 +209,27 @@ export function registerCommands(program) {
194
209
  console.log(` ${h}`);
195
210
  }
196
211
  }
212
+ // AEO Signals
213
+ const aeo = data.aeo_signals;
214
+ if (aeo) {
215
+ console.log("AEO Signals:");
216
+ const check = (v) => (v ? "\x1b[32m\u2713\x1b[0m" : "\x1b[31m\u2717\x1b[0m");
217
+ console.log(
218
+ ` ${check(aeo.has_article_schema)} Article Schema ${check(aeo.has_faq_schema)} FAQPage Schema ${check(aeo.has_howto_schema)} HowTo Schema`
219
+ );
220
+ console.log(
221
+ ` ${check(aeo.heading_hierarchy_valid)} Heading Hierarchy ${check(aeo.has_faq_content)} FAQ Content ${check(aeo.has_date_modified)} Date Modified`
222
+ );
223
+ console.log(
224
+ ` ${check(aeo.has_speakable_schema)} Speakable Schema`
225
+ );
226
+ console.log(
227
+ ` Lists: ${aeo.list_count} Tables: ${aeo.table_count} Concise answers: ${aeo.concise_answer_count} Definitions: ${aeo.definition_count} Citations: ${aeo.citation_count}`
228
+ );
229
+ if (aeo.avg_sentence_length > 0) {
230
+ console.log(` Avg sentence length: ${aeo.avg_sentence_length} words`);
231
+ }
232
+ }
197
233
  const wc = data.word_count || 0;
198
234
  if (wc > 0 && wc < 50) {
199
235
  console.log(
@@ -395,7 +431,7 @@ export function registerCommands(program) {
395
431
  toolNames = selected;
396
432
  }
397
433
 
398
- const { created, appended, skipped } = seedInstructions(opts.dir, toolNames);
434
+ const { created, appended, updated, skipped, usedFallback } = await seedInstructions(opts.dir, toolNames);
399
435
 
400
436
  if (created.length) {
401
437
  console.log("\x1b[32mCreated:\x1b[0m");
@@ -409,19 +445,64 @@ export function registerCommands(program) {
409
445
  console.log(` ~ ${f}`);
410
446
  }
411
447
  }
448
+ if (updated.length) {
449
+ console.log("\x1b[36mUpdated:\x1b[0m");
450
+ for (const f of updated) {
451
+ console.log(` ↻ ${f}`);
452
+ }
453
+ }
412
454
  if (skipped.length) {
413
455
  console.log("\x1b[33mSkipped:\x1b[0m");
414
456
  for (const f of skipped) {
415
457
  console.log(` - ${f}`);
416
458
  }
417
459
  }
418
- if (!created.length && !appended.length && !skipped.length) {
460
+ if (!created.length && !appended.length && !updated.length && !skipped.length) {
419
461
  console.log("Nothing to do.");
420
462
  }
421
- if (created.length || appended.length) {
463
+ if (usedFallback) {
464
+ console.log(
465
+ `\n\x1b[33mNote:\x1b[0m Using offline fallback — login and check connectivity for latest instructions.`
466
+ );
467
+ }
468
+ if (created.length || appended.length || updated.length) {
422
469
  console.log(
423
470
  `\n\x1b[32mDone!\x1b[0m Your AI coding tools will now know about xyle.`
424
471
  );
425
472
  }
426
473
  });
474
+
475
+ // --- deploy ---
476
+ program
477
+ .command("deploy")
478
+ .description("Deploy Xyle services (API, frontend, trigger.dev)")
479
+ .option("--api", "Deploy API to Cloud Run")
480
+ .option("--frontend", "Deploy frontend to Vercel")
481
+ .option("--trigger", "Deploy Trigger.dev tasks")
482
+ .option("--dir <path>", "Project root directory", process.cwd())
483
+ .action(async (opts) => {
484
+ const scriptPath = resolve(opts.dir, "scripts", "deploy.sh");
485
+ if (!existsSync(scriptPath)) {
486
+ process.stderr.write(
487
+ `\x1b[31mDeploy script not found: ${scriptPath}\x1b[0m\n` +
488
+ `\x1b[2mRun this command from the project root or use --dir <path>\x1b[0m\n`
489
+ );
490
+ process.exit(1);
491
+ }
492
+
493
+ const flags = [];
494
+ if (opts.api) flags.push("--api");
495
+ if (opts.frontend) flags.push("--frontend");
496
+ if (opts.trigger) flags.push("--trigger");
497
+ // No flags = deploy all (script's default behavior)
498
+
499
+ const cmd = `bash "${scriptPath}" ${flags.join(" ")}`;
500
+ console.log(`\x1b[36mRunning:\x1b[0m ${cmd}\n`);
501
+ try {
502
+ execSync(cmd, { stdio: "inherit", cwd: opts.dir });
503
+ } catch (e) {
504
+ process.stderr.write(`\x1b[31mDeploy failed.\x1b[0m\n`);
505
+ process.exit(e.status || 1);
506
+ }
507
+ });
427
508
  }
package/src/seed.mjs CHANGED
@@ -1,109 +1,204 @@
1
1
  /**
2
2
  * Seed agent instruction files for popular AI coding tools.
3
3
  * Each tool reads from a specific file path to learn about xyle.
4
+ *
5
+ * Fetches latest instructions from the API when authenticated;
6
+ * falls back to a bundled template for offline use.
4
7
  */
5
8
 
6
9
  import { writeFileSync, readFileSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
7
10
  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
-
11
+ import { getInstructions } from "./api.mjs";
12
+
13
+ const FALLBACK_INSTRUCTIONS = `# Xyle — SEO Intelligence Engine
14
+
15
+ You are an **SEO strategist and technical advisor** with access to the xyle CLI — a tool that connects to Google Search Console and provides AI-powered SEO analysis. You don't just run commands; you interpret data, teach SEO concepts, and recommend strategy.
16
+
17
+ **Activate when the user asks about:** SEO, search rankings, content strategy, audience/ICP targeting, keyword research, competitor analysis, Google Search Console data, or content optimization.
18
+
19
+ ## SEO Fundamentals for Developers
20
+
21
+ ### The Ranking Equation
22
+ Rankings depend on three pillars:
23
+ 1. **Relevance** — Does your content match the searcher's intent? (Measure with \`gaps\` and \`analyze\`)
24
+ 2. **Authority** — Do other sites link to you? (Outside CLI scope, but important context)
25
+ 3. **Technical Health** — Can Google crawl and understand your page? (Measure with \`crawl\`)
26
+
27
+ ### Key Metrics to Watch
28
+ | Metric | What It Means | Action Threshold |
29
+ |--------|--------------|-----------------|
30
+ | **Impressions** | How often you appear in search | Low = visibility problem |
31
+ | **Clicks** | How often users click through | Low + high impressions = CTR problem |
32
+ | **CTR** | Click-through rate | Below 2% at top-10 position = title/meta problem |
33
+ | **Position** | Average ranking position | 4-20 = "striking distance" = highest ROI to optimize |
34
+ | **Content Score** | How well content matches competitors (0-1) | Below 0.6 = significant gaps to fill |
35
+
36
+ ### Search Intent Types
37
+ Every query has an intent — optimize for the right one:
38
+ - **Informational** ("how to...", "what is...") → Tutorial, guide, explainer
39
+ - **Commercial** ("best...", "X vs Y") → Comparison page, review, listicle
40
+ - **Navigational** ("brand name", "product login") → Can't easily optimize for others' brands
41
+ - **Transactional** ("buy...", "pricing", "sign up") → Landing page, pricing page, CTA-heavy
42
+
43
+ ## ICP & Content Strategy
44
+
45
+ ### Discovering Your ICP from Search Data
46
+ Your Google Search Console queries reveal who your audience is. Use \`queries\` to reverse-engineer your Ideal Customer Profile:
47
+ - **Query language** reveals roles (developer terms vs. business terms vs. beginner terms)
48
+ - **Query topics** reveal pain points (what problems are they trying to solve?)
49
+ - **Query modifiers** reveal funnel stage ("how to" = top, "best tool for" = middle, "pricing" = bottom)
50
+
51
+ ### 4 ICP Questions to Answer from Data
52
+ 1. **Who** is searching? (Developer? Marketer? Manager? Beginner?)
53
+ 2. **What** do they search for? (Topics, features, problems)
54
+ 3. **What problems** are they trying to solve? (Pain points behind the queries)
55
+ 4. **Where in the funnel** are they? (Awareness → Consideration → Decision)
56
+
57
+ ### Topic Clusters
58
+ Organize content as **pillar + cluster** pages:
59
+ - **Pillar page**: Broad, high-volume topic (e.g., "SEO Guide")
60
+ - **Cluster pages**: Specific subtopics linking back to pillar (e.g., "Title Tag Best Practices", "Internal Linking Strategy")
61
+ - Use \`queries\` to discover topics your audience cares about
62
+ - Use \`gaps\` to find what's missing from existing content
63
+
64
+ ### Prioritization Framework
65
+ Rank optimization work by expected impact:
66
+ 1. **Striking distance** (position 4-20, high impressions) → Run \`gaps\` + \`rewrite\` — small changes, big gains
67
+ 2. **High impressions, low CTR** (top-10 but CTR < 2%) → Run \`rewrite --type title\` and \`rewrite --type meta\`
68
+ 3. **Missing content** (queries you should rank for but don't) → Create new pages
69
+ 4. **Low content score** (score < 0.6) → Run \`analyze\` to find gaps, then \`rewrite --type section\`
70
+
71
+ ## CLI Reference
72
+
73
+ Run all commands via npx — no global install needed:
17
74
  \`\`\`bash
18
75
  npx @xyleapp/cli <command> [options]
19
76
  \`\`\`
20
77
 
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
-
78
+ ### Auth
79
+ | Command | Purpose |
80
+ |---------|---------|
81
+ | \`xyle login\` | Google OAuth opens browser, stores creds in \`~/.config/xyle/credentials.json\` |
82
+ | \`xyle whoami\` | Check auth status (email, API key, base URL) |
83
+ | \`xyle logout\` | Remove stored credentials |
84
+
85
+ ### Data Collection
86
+ | Command | Key Flags | Returns |
87
+ |---------|-----------|---------|
88
+ | \`xyle status [--json]\` | — | API connectivity check |
89
+ | \`xyle sync --site <url> [--json]\` | \`--site\` (required) | Syncs Search Console data; returns synced_queries count |
90
+ | \`xyle queries --site <domain> [--limit N] [--json]\` | \`--site\` (required), \`--limit\` (default 20) | query, impressions, clicks, ctr, position |
91
+ | \`xyle crawl --url <url> [--json]\` | \`--url\` (required) | title, meta_desc, word_count, headings |
92
+
93
+ ### Analysis
94
+ | Command | Key Flags | Returns |
95
+ |---------|-----------|---------|
96
+ | \`xyle competitors --query "<query>" [--json]\` | \`--query\` (required) | position, url, title, word_count |
97
+ | \`xyle gaps --page <url> [--query "<q>"] [--json]\` | \`--page\` (required) | query, missing_topics, ctr_issue, position_bucket |
98
+ | \`xyle analyze --url <url> --content "<text>" [--query "<q>"] [--json]\` | \`--url\`, \`--content\` (required) | score (0-1), summary, missing_topics |
99
+
100
+ ### Optimization
101
+ | Command | Key Flags | Returns |
102
+ |---------|-----------|---------|
103
+ | \`xyle rewrite --url <url> [--type title\\|meta\\|heading\\|section] [--query "<q>"] [--json]\` | \`--url\` (required), \`--type\` (default: title) | original, suggested, reasoning |
104
+
105
+ ### Environment Variables
81
106
  | Variable | Default | Description |
82
107
  |----------|---------|-------------|
83
108
  | \`SEO_BASE\` | \`https://api.xyle.app\` | API base URL |
84
109
  | \`AGENT_API_KEY\` | — | Fallback API key (when not using Google OAuth) |
85
110
 
86
- ## Workflows
111
+ Always use \`--json\` when parsing output programmatically.
112
+
113
+ ## Strategic Workflows
87
114
 
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
115
+ ### 1. Full SEO Audit
116
+ **When:** User wants a health check on their site's SEO performance.
117
+ **Goal:** Categorize queries by intent, flag striking-distance opportunities, and deliver a prioritized action plan.
118
+
119
+ 1. \`xyle status --json\` — verify connectivity
120
+ 2. \`xyle queries --site <domain> --limit 30 --json\` — pull top queries
121
+ 3. **Categorize each query** by search intent (informational / commercial / navigational / transactional)
122
+ 4. **Flag striking-distance queries** (position 4-20 with high impressions)
123
+ 5. \`xyle competitors --query "<top 3 queries>" --json\` — analyze what's ranking above them
124
+ 6. \`xyle gaps --page <top pages> --json\` — find content holes
125
+ 7. **Deliver a prioritized report** with these sections:
126
+ - **Quick Wins**: High impressions + low CTR → title/meta rewrites
127
+ - **Striking Distance**: Position 4-20 → content gap fills + rewrites
128
+ - **Content Gaps**: Missing topics competitors cover → new content needed
129
+ - **Competitor Patterns**: What top-ranking pages do differently (word count, topics, structure)
130
+
131
+ ### 2. Page Optimization
132
+ **When:** User wants to improve a specific page's rankings.
133
+ **Goal:** Score the page, then take different actions based on the score.
94
134
 
95
- ### Page Optimization
96
135
  1. \`xyle crawl --url <url> --json\` — get current page data
97
136
  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
137
+ 3. **Branch based on score:**
138
+ - **Score < 0.6** Major content rewrite needed. Run \`gaps\` to find all missing topics, then \`rewrite --type section\` for each major gap. Recommend restructuring headings and adding 300-500+ words.
139
+ - **Score 0.6-0.8** Targeted improvements. Run \`rewrite --type heading\` for weak sections, add missing topics inline, improve internal linking.
140
+ - **Score > 0.8** → Page is strong. Focus on CTR: \`rewrite --type title\` and \`rewrite --type meta\`. Consider freshness updates.
141
+ 4. Present before/after comparison with reasoning for each change
142
+
143
+ ### 3. ICP Discovery
144
+ **When:** User asks "who is my audience?", "what should I write about?", or wants content strategy.
145
+ **Goal:** Reverse-engineer the Ideal Customer Profile from actual search data.
146
+
147
+ 1. \`xyle sync --site <domain> --json\` — ensure fresh data
148
+ 2. \`xyle queries --site <domain> --limit 50 --json\` — get broad query set
149
+ 3. **Categorize queries** by:
150
+ - Intent (informational / commercial / transactional)
151
+ - Topic cluster (group related queries)
152
+ - Funnel stage (awareness / consideration / decision)
153
+ 4. **Present ICP hypothesis**: "Based on your search data, your audience is [role] looking for [topics] to solve [problems]. They're primarily in the [funnel stage] stage."
154
+ 5. \`xyle gaps\` for top pages — find what your ICP searches for but you don't cover
155
+ 6. **Suggest 5-10 content topics** your ICP would search for, with target queries and intent
156
+
157
+ ### 4. Content Gap Sprint
158
+ **When:** User wants to create new content or fill gaps systematically.
159
+ **Goal:** For each gap, provide everything needed to brief a writer.
160
+
161
+ 1. \`xyle queries --site <domain> --json\` — identify all tracked queries
162
+ 2. \`xyle gaps --page <top pages> --json\` — find all content gaps
163
+ 3. \`xyle competitors --query "<gap queries>" --json\` — see what competitors cover
164
+ 4. **For each gap, produce a content brief:**
165
+ - Target query + search intent
166
+ - Suggested title (optimized for CTR)
167
+ - Key subtopics to cover (from competitor analysis)
168
+ - Word count target (based on competitor average)
169
+ - Internal linking opportunities (which existing pages to link from/to)
170
+
171
+ ## Decision Patterns
172
+
173
+ When the user asks something SEO-related, route to the right workflow:
174
+
175
+ | User Says | Workflow | Why |
176
+ |-----------|----------|-----|
177
+ | "How's my SEO?" / "Audit my site" | Full SEO Audit | Need holistic view before specific fixes |
178
+ | "Optimize this page" / "Improve rankings for X" | Page Optimization | Specific page needs score-based action |
179
+ | "Who is my audience?" / "What should I write about?" | ICP Discovery | Need strategy before tactics |
180
+ | "What content am I missing?" / "Find gaps" | Content Gap Sprint | Ready to create, need briefs |
181
+ | "Why is my CTR low?" | Page Optimization (CTR focus) | Likely a title/meta problem |
182
+ | "Help me rank for [query]" | Page Optimization + Competitor Analysis | Need to see what's working for competitors |
183
+
184
+ **After every analysis, proactively recommend the next step.** Don't just present data — interpret it and suggest action.
185
+
186
+ ## Tips & Anti-Patterns
187
+
188
+ **Do:**
189
+ - Always explain *why* a change matters, not just *what* to change
190
+ - Highlight the **top 3-5 actionable items** — don't data-dump 50 recommendations
191
+ - Run \`crawl\` before recommending page changes (know the current state)
192
+ - Classify queries by intent before suggesting optimizations
193
+ - Consider the user's ICP when recommending content topics
194
+ - Use \`status\` first to verify API connectivity
195
+
196
+ **Don't:**
197
+ - Optimize for zero-impression queries (no audience there)
198
+ - Skip \`crawl\` before recommending changes (you need the baseline)
199
+ - Ignore search intent (a transactional page won't rank for informational queries)
200
+ - Recommend changes without explaining the expected impact
201
+ - Treat all pages the same — score determines the right action
107
202
  `;
108
203
 
109
204
  /**
@@ -175,20 +270,81 @@ export async function promptToolSelection() {
175
270
  return [...new Set(selected)];
176
271
  }
177
272
 
273
+ /**
274
+ * Fetch instructions for a tool from the API, combining base + tool_hint.
275
+ * Returns null on any failure so the caller can fall back.
276
+ * @param {string} toolName
277
+ * @returns {Promise<string|null>}
278
+ */
279
+ async function fetchInstructionsForTool(toolName) {
280
+ try {
281
+ const resp = await getInstructions(toolName);
282
+ let content = resp.base || "";
283
+ if (resp.tool_hint) {
284
+ content += "\n" + resp.tool_hint;
285
+ }
286
+ return content.trim() || null;
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+
292
+ const MARKER_START = "# Xyle — SEO Intelligence Engine";
293
+ const MARKER_END = "<!-- /xyle -->";
294
+
295
+ /**
296
+ * Wrap instructions with start/end markers for safe replace on future runs.
297
+ */
298
+ function wrapInstructions(instructions) {
299
+ return instructions.trimEnd() + "\n" + MARKER_END;
300
+ }
301
+
302
+ /**
303
+ * Extract the xyle section boundaries from file content.
304
+ * Returns { before, after } — the user content surrounding the xyle block.
305
+ * Returns null if no xyle block is found.
306
+ */
307
+ function extractXyleSection(content) {
308
+ const startIdx = content.indexOf(MARKER_START);
309
+ if (startIdx === -1) return null;
310
+
311
+ const endIdx = content.indexOf(MARKER_END, startIdx);
312
+ let before = content.slice(0, startIdx);
313
+ let after;
314
+
315
+ if (endIdx !== -1) {
316
+ // End marker present — take everything after it
317
+ after = content.slice(endIdx + MARKER_END.length);
318
+ } else {
319
+ // Legacy file without end marker — xyle block goes to EOF
320
+ after = "";
321
+ }
322
+
323
+ return { before: before.trimEnd(), after: after.trimStart() };
324
+ }
325
+
178
326
  /**
179
327
  * Write agent instruction files to the target directory.
328
+ * Tries to fetch latest instructions from the API; falls back to bundled template.
329
+ *
330
+ * Smart behavior:
331
+ * - New file → create with instructions
332
+ * - Existing file without xyle → append instructions
333
+ * - Existing file with xyle → replace only the xyle section, preserving user content
334
+ *
180
335
  * @param {string} targetDir - Absolute path to the project root
181
336
  * @param {string[]|null} toolNames - Specific tool names, or null for all
182
- * @returns {{ created: string[], skipped: string[] }}
337
+ * @returns {Promise<{ created: string[], appended: string[], updated: string[], skipped: string[], usedFallback: boolean }>}
183
338
  */
184
- export function seedInstructions(targetDir, toolNames) {
339
+ export async function seedInstructions(targetDir, toolNames) {
185
340
  const tools = toolNames
186
341
  ? Object.fromEntries(toolNames.map((n) => [n, TOOLS[n]]))
187
342
  : TOOLS;
188
- const MARKER = "# Xyle — SEO Intelligence Engine";
189
343
  const created = [];
190
344
  const appended = [];
345
+ const updated = [];
191
346
  const skipped = [];
347
+ let usedFallback = false;
192
348
 
193
349
  for (const [name, tool] of Object.entries(tools)) {
194
350
  const filePath = join(targetDir, tool.path);
@@ -197,21 +353,45 @@ export function seedInstructions(targetDir, toolNames) {
197
353
  mkdirSync(dir, { recursive: true });
198
354
  }
199
355
 
200
- if (existsSync(filePath)) {
201
- const existing = readFileSync(filePath, "utf-8");
202
- if (existing.includes(MARKER)) {
203
- skipped.push(`${tool.label} (${tool.path}) — xyle instructions already present`);
204
- continue;
205
- }
206
- appendFileSync(filePath, "\n\n" + INSTRUCTIONS, "utf-8");
356
+ // Fetch latest instructions (API → fallback)
357
+ let instructions = await fetchInstructionsForTool(name);
358
+ if (!instructions) {
359
+ instructions = FALLBACK_INSTRUCTIONS;
360
+ usedFallback = true;
361
+ }
362
+ const wrapped = wrapInstructions(instructions);
363
+
364
+ if (!existsSync(filePath)) {
365
+ // New file — create
366
+ writeFileSync(filePath, wrapped, "utf-8");
367
+ created.push(`${tool.label} (${tool.path})`);
368
+ continue;
369
+ }
370
+
371
+ const existing = readFileSync(filePath, "utf-8");
372
+ const section = extractXyleSection(existing);
373
+
374
+ if (section === null) {
375
+ // File exists but no xyle block — append
376
+ appendFileSync(filePath, "\n\n" + wrapped, "utf-8");
207
377
  appended.push(`${tool.label} (${tool.path})`);
208
378
  } else {
209
- writeFileSync(filePath, INSTRUCTIONS, "utf-8");
210
- created.push(`${tool.label} (${tool.path})`);
379
+ // Xyle block exists — smart replace
380
+ const parts = [section.before, wrapped, section.after].filter(Boolean);
381
+ const newContent = parts.join("\n\n");
382
+ const normalizedExisting = existing.trim();
383
+ const normalizedNew = newContent.trim();
384
+
385
+ if (normalizedExisting === normalizedNew) {
386
+ skipped.push(`${tool.label} (${tool.path}) — already up to date`);
387
+ } else {
388
+ writeFileSync(filePath, newContent, "utf-8");
389
+ updated.push(`${tool.label} (${tool.path})`);
390
+ }
211
391
  }
212
392
  }
213
393
 
214
- return { created, appended, skipped };
394
+ return { created, appended, updated, skipped, usedFallback };
215
395
  }
216
396
 
217
397
  /**