geo-ai-search-optimization 1.4.1 → 1.4.2

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/README.md CHANGED
@@ -100,6 +100,23 @@ geo-ai-search-optimization rewrite-pack ./content/posts/geo-guide.mdx --json --o
100
100
  - execution checklist
101
101
  - 可直接交给 agent 的 rewrite prompt
102
102
 
103
+ ## Repo Patch Plan 命令
104
+
105
+ 如果你希望从“页面建议”继续推进到“仓库里先改哪些文件和模板”,可以直接用 `repo-patch-plan`:
106
+
107
+ ```bash
108
+ geo-ai-search-optimization repo-patch-plan ./your-site
109
+ geo-ai-search-optimization repo-patch-plan ./your-site --json --out ./reports/repo-patch-plan.json
110
+ ```
111
+
112
+ 它会输出:
113
+
114
+ - 候选文件区域
115
+ - patch packets
116
+ - likely files
117
+ - execution notes
118
+ - 可直接交给 agent 的 repo-level prompt
119
+
103
120
  ## Agent Resume 命令
104
121
 
105
122
  如果 GEO 工作已经做过一轮或多轮,你不想让下一个 agent 从头重新判断,而是想让它从最近一个可靠恢复点继续,可以直接用 `agent-resume`:
@@ -988,6 +1005,12 @@ geo-ai-search-optimization help
988
1005
  - 基于 `page-audit` 继续产出 metadata drafts、章节结构、FAQ questions、schema recommendations 与 execution checklist
989
1006
  - 让产品从“页面级诊断”继续推进到“页面级改写方案”
990
1007
 
1008
+ ## New in 1.4.2
1009
+
1010
+ - 新增 `repo-patch-plan`
1011
+ - 把 GEO 问题继续收敛到仓库级 likely files、模板入口、metadata / schema / navigation 区域
1012
+ - 让 agent 更容易从“页面建议”切到“仓库改动计划”
1013
+
991
1014
  ## New in 1.2.20
992
1015
 
993
1016
  - 新增 `agent-continue`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geo-ai-search-optimization",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Install and run a Generative Engine Optimization (GEO)-first, SEO-supported Codex skill for website optimization.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@ import {
15
15
  import { createLlmsTxt } from "./llms-txt.js";
16
16
  import { auditPage, renderPageAuditMarkdown, writePageAuditOutput } from "./page-audit.js";
17
17
  import { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
18
+ import { createRepoPatchPlan, renderRepoPatchPlanMarkdown, writeRepoPatchPlanOutput } from "./repo-patch-plan.js";
18
19
  import { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput } from "./rewrite-pack.js";
19
20
  import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
20
21
  import { createSchemaTemplate } from "./schema.js";
@@ -27,6 +28,7 @@ export const SITE_OPS_HELP_LINES = [
27
28
  " geo-ai-search-optimization onboard-url <website-url> [--json] [--out <file>]",
28
29
  " geo-ai-search-optimization page-audit <url-or-file> [--json] [--out <file>]",
29
30
  " geo-ai-search-optimization rewrite-pack <url-or-file> [--json] [--out <file>]",
31
+ " geo-ai-search-optimization repo-patch-plan <project-path> [--json] [--out <file>]",
30
32
  " geo-ai-search-optimization init-llms [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
31
33
  " geo-ai-search-optimization init-schema <type> [target-dir] [--site-url <url>] [--overwrite] [--json]",
32
34
  " geo-ai-search-optimization audit <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
@@ -109,6 +111,28 @@ const handleRewritePack = createStructuredOutputCommandHandler({
109
111
  getOutputJson: (args) => hasFlag(args, "--json")
110
112
  });
111
113
 
114
+ const handleRepoPatchPlan = createStructuredOutputCommandHandler({
115
+ commandLabel: "repo patch plan",
116
+ execute: async (args) => {
117
+ const input = args.find((value) => !value.startsWith("-"));
118
+ if (!input) {
119
+ throw new Error("repo-patch-plan requires a project path");
120
+ }
121
+
122
+ return createRepoPatchPlan(input, {
123
+ maxFileSize: getFlagValue(args, "--max-file-size")
124
+ ? parsePositiveInteger(getFlagValue(args, "--max-file-size"), "--max-file-size")
125
+ : undefined,
126
+ maxExamples: getFlagValue(args, "--max-examples")
127
+ ? parsePositiveInteger(getFlagValue(args, "--max-examples"), "--max-examples")
128
+ : undefined
129
+ });
130
+ },
131
+ renderMarkdown: (report) => `${renderRepoPatchPlanMarkdown(report)}\n`,
132
+ writeOutput: writeRepoPatchPlanOutput,
133
+ getOutputJson: (args) => hasFlag(args, "--json")
134
+ });
135
+
112
136
  const handleInitLlms = createActionCommandHandler({
113
137
  execute: async (args) =>
114
138
  createLlmsTxt({
@@ -189,6 +213,7 @@ export const SITE_OPS_COMMAND_HANDLERS = {
189
213
  "onboard-url": handleOnboardUrl,
190
214
  "page-audit": handlePageAudit,
191
215
  "rewrite-pack": handleRewritePack,
216
+ "repo-patch-plan": handleRepoPatchPlan,
192
217
  "init-llms": handleInitLlms,
193
218
  "init-schema": handleInitSchema,
194
219
  audit: handleAudit,
package/src/index.js CHANGED
@@ -42,6 +42,7 @@ export { createOwnerBoard, renderOwnerBoardMarkdown, writeOwnerBoardOutput } fro
42
42
  export { createPmBrief, renderPmBriefMarkdown, writePmBriefOutput } from "./pm-brief.js";
43
43
  export { renderPublishPackMarkdown, writePublishPack } from "./publish-pack.js";
44
44
  export { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
45
+ export { createRepoPatchPlan, renderRepoPatchPlanMarkdown, writeRepoPatchPlanOutput } from "./repo-patch-plan.js";
45
46
  export { generateReport, writeReportOutput } from "./report.js";
46
47
  export { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput } from "./rewrite-pack.js";
47
48
  export { createRoadmap, renderRoadmapMarkdown, writeRoadmapOutput } from "./roadmap.js";
@@ -0,0 +1,257 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { auditProject } from "./audit.js";
4
+ import { writeScanOutput } from "./scan.js";
5
+
6
+ const CANDIDATE_PATTERNS = [
7
+ {
8
+ area: "metadata",
9
+ label: "metadata / SEO helper",
10
+ test: (filePath) =>
11
+ /(seo|meta|metadata|head|layout|rootlayout|app\.tsx|app\.jsx|document|helmet)/i.test(filePath)
12
+ },
13
+ {
14
+ area: "schema",
15
+ label: "schema / structured data",
16
+ test: (filePath) => /(schema|json-ld|structured-data)/i.test(filePath)
17
+ },
18
+ {
19
+ area: "content-template",
20
+ label: "content template",
21
+ test: (filePath) => /(post|article|blog|page|content|docs|template|route|slug)/i.test(filePath)
22
+ },
23
+ {
24
+ area: "navigation",
25
+ label: "navigation / sitemap / robots",
26
+ test: (filePath) => /(robots|sitemap|llms|routes|navigation|nav)/i.test(filePath)
27
+ }
28
+ ];
29
+
30
+ const TEXT_EXTENSIONS = new Set([
31
+ ".astro",
32
+ ".html",
33
+ ".js",
34
+ ".jsx",
35
+ ".md",
36
+ ".mdx",
37
+ ".mjs",
38
+ ".svelte",
39
+ ".ts",
40
+ ".tsx",
41
+ ".vue"
42
+ ]);
43
+
44
+ const IGNORE_DIRS = new Set([
45
+ ".git",
46
+ ".next",
47
+ ".nuxt",
48
+ ".output",
49
+ ".svelte-kit",
50
+ "build",
51
+ "coverage",
52
+ "dist",
53
+ "node_modules",
54
+ "out",
55
+ "tmp"
56
+ ]);
57
+
58
+ async function collectRepoFiles(root) {
59
+ const files = [];
60
+ const queue = [root];
61
+
62
+ while (queue.length > 0) {
63
+ const current = queue.shift();
64
+ const entries = await fs.readdir(current, { withFileTypes: true });
65
+
66
+ for (const entry of entries) {
67
+ const entryPath = path.join(current, entry.name);
68
+ if (entry.isDirectory()) {
69
+ if (!IGNORE_DIRS.has(entry.name)) {
70
+ queue.push(entryPath);
71
+ }
72
+ continue;
73
+ }
74
+
75
+ const extension = path.extname(entry.name).toLowerCase();
76
+ if (TEXT_EXTENSIONS.has(extension) || entry.name === "robots.txt" || entry.name === "llms.txt") {
77
+ files.push(entryPath);
78
+ }
79
+ }
80
+ }
81
+
82
+ return files;
83
+ }
84
+
85
+ function rankCandidateFiles(root, files) {
86
+ return CANDIDATE_PATTERNS.map((pattern) => {
87
+ const matches = files
88
+ .filter((filePath) => pattern.test(path.relative(root, filePath)))
89
+ .slice(0, 6)
90
+ .map((filePath) => path.relative(root, filePath));
91
+
92
+ return {
93
+ area: pattern.area,
94
+ label: pattern.label,
95
+ files: matches
96
+ };
97
+ }).filter((entry) => entry.files.length > 0);
98
+ }
99
+
100
+ function buildPatchPackets(auditReport, candidates) {
101
+ const packets = [];
102
+
103
+ if (auditReport.groups.blockers.length > 0) {
104
+ packets.push({
105
+ id: "repo-fix-01",
106
+ priority: "P0",
107
+ owner: "工程 / SEO",
108
+ title: "先修基础抓取与归一入口",
109
+ targetArea: "抓取与索引",
110
+ likelyFiles: candidates.find((item) => item.area === "navigation")?.files || [],
111
+ actions: auditReport.groups.blockers.slice(0, 4),
112
+ doneWhen: "robots.txt、sitemap、canonical 机制可在仓库中找到明确入口并已接通。"
113
+ });
114
+ }
115
+
116
+ if (auditReport.groups.highImpactFixes.length > 0) {
117
+ packets.push({
118
+ id: "repo-fix-02",
119
+ priority: "P1",
120
+ owner: "工程 / 内容 / SEO",
121
+ title: "补齐页面模板中的 GEO 核心信号",
122
+ targetArea: "模板与页面结构",
123
+ likelyFiles: [
124
+ ...(candidates.find((item) => item.area === "metadata")?.files || []),
125
+ ...(candidates.find((item) => item.area === "content-template")?.files || [])
126
+ ].slice(0, 8),
127
+ actions: auditReport.groups.highImpactFixes.slice(0, 5),
128
+ doneWhen: "metadata、answer-first opening、作者/时间、来源链接和关键模板入口已能统一治理。"
129
+ });
130
+ }
131
+
132
+ if (auditReport.groups.supportFixes.length > 0) {
133
+ packets.push({
134
+ id: "repo-fix-03",
135
+ priority: "P2",
136
+ owner: "工程 / SEO",
137
+ title: "补模板级辅助信号",
138
+ targetArea: "模板治理",
139
+ likelyFiles: [
140
+ ...(candidates.find((item) => item.area === "schema")?.files || []),
141
+ ...(candidates.find((item) => item.area === "metadata")?.files || [])
142
+ ].slice(0, 8),
143
+ actions: auditReport.groups.supportFixes.slice(0, 4),
144
+ doneWhen: "模板层的 metadata、breadcrumb、schema 和 comparison 支持已有明确实现入口。"
145
+ });
146
+ }
147
+
148
+ return packets;
149
+ }
150
+
151
+ function buildExecutionNotes(auditReport, candidates) {
152
+ const notes = [];
153
+
154
+ if (candidates.find((item) => item.area === "metadata")) {
155
+ notes.push("优先检查 metadata / head / layout / SEO helper,不要逐页手改。");
156
+ }
157
+ if (candidates.find((item) => item.area === "schema")) {
158
+ notes.push("如果仓库已有 schema 入口,先复用现有 schema builder,再补页面类型逻辑。");
159
+ }
160
+ if (candidates.find((item) => item.area === "content-template")) {
161
+ notes.push("优先改文章页、内容页或动态路由模板,再考虑单页补丁。");
162
+ }
163
+ if (auditReport.groups.highImpactFixes.length > 0) {
164
+ notes.push("先处理高影响 GEO 修复项,再进入支持性优化,避免工程分散。");
165
+ }
166
+
167
+ return notes;
168
+ }
169
+
170
+ export async function createRepoPatchPlan(rootInput, options = {}) {
171
+ const root = path.resolve(rootInput);
172
+ const stat = await fs.stat(root).catch(() => null);
173
+ if (!stat || !stat.isDirectory()) {
174
+ throw new Error("repo-patch-plan 需要一个本地项目目录");
175
+ }
176
+
177
+ const [auditReport, repoFiles] = await Promise.all([
178
+ auditProject(root, options),
179
+ collectRepoFiles(root)
180
+ ]);
181
+
182
+ const candidates = rankCandidateFiles(root, repoFiles);
183
+ const patchPackets = buildPatchPackets(auditReport, candidates);
184
+
185
+ return {
186
+ kind: "geo-repo-patch-plan",
187
+ root,
188
+ summary: "这份 repo patch plan 会把 GEO 问题尽量落到仓库级模板、metadata、schema 和路由入口,方便 agent 直接开始改代码。",
189
+ score: {
190
+ value: auditReport.score,
191
+ maxScore: auditReport.maxScore,
192
+ label: auditReport.scoreLabel
193
+ },
194
+ candidateFiles: candidates,
195
+ patchPackets,
196
+ executionNotes: buildExecutionNotes(auditReport, candidates),
197
+ auditReport,
198
+ agentPrompt: [
199
+ "请把这次 GEO 修复当成仓库级任务处理。",
200
+ `项目目录:${root}`,
201
+ `当前 GEO 分数:${auditReport.score}/${auditReport.maxScore}(${auditReport.scoreLabel})`,
202
+ `优先包:${patchPackets[0]?.id || "repo-fix-01"}`,
203
+ "先从 likelyFiles 开始确认 metadata、schema、content template、navigation 入口。",
204
+ "优先复用模板和 helper,不要只改单个页面。",
205
+ "完成一包后重新跑 audit 或 page-audit,验证问题是否实际减少。"
206
+ ].join(" ")
207
+ };
208
+ }
209
+
210
+ export function renderRepoPatchPlanMarkdown(plan) {
211
+ const lines = [
212
+ "# GEO Repo Patch Plan",
213
+ "",
214
+ `- 项目目录:\`${plan.root}\``,
215
+ `- 当前分数:\`${plan.score.value}/${plan.score.maxScore}\`(${plan.score.label})`,
216
+ `- 总结:${plan.summary}`,
217
+ "",
218
+ "## 候选文件区域",
219
+ ""
220
+ ];
221
+
222
+ for (const candidate of plan.candidateFiles) {
223
+ lines.push(`- ${candidate.label}`);
224
+ for (const file of candidate.files) {
225
+ lines.push(` - ${file}`);
226
+ }
227
+ }
228
+
229
+ lines.push("", "## Patch Packets", "");
230
+ for (const packet of plan.patchPackets) {
231
+ lines.push(`- ${packet.id}|${packet.priority}|${packet.owner}|${packet.title}`);
232
+ lines.push(` Target:${packet.targetArea}`);
233
+ lines.push(` Done when:${packet.doneWhen}`);
234
+ if (packet.likelyFiles.length > 0) {
235
+ lines.push(" Likely files:");
236
+ for (const file of packet.likelyFiles) {
237
+ lines.push(` - ${file}`);
238
+ }
239
+ }
240
+ lines.push(" Actions:");
241
+ for (const action of packet.actions) {
242
+ lines.push(` - ${action}`);
243
+ }
244
+ }
245
+
246
+ lines.push("", "## Execution Notes", "");
247
+ for (const note of plan.executionNotes) {
248
+ lines.push(`- ${note}`);
249
+ }
250
+
251
+ lines.push("", "## Agent Prompt", "", plan.agentPrompt, "");
252
+ return lines.join("\n");
253
+ }
254
+
255
+ export async function writeRepoPatchPlanOutput(outputPath, content) {
256
+ return writeScanOutput(outputPath, content);
257
+ }