n-seo 0.1.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/.env.example +13 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/n-seo.mjs +310 -0
- package/docs/ADDING-A-SITE.md +82 -0
- package/docs/ARCHITECTURE.md +213 -0
- package/docs/DEPLOY.md +300 -0
- package/docs/FAQ.md +93 -0
- package/docs/INSTANCE.md +365 -0
- package/docs/MCP.md +104 -0
- package/docs/OPERATING-RULES.md +106 -0
- package/docs/PLAYBOOK.md +122 -0
- package/docs/PRD.md +249 -0
- package/docs/RELEASING.md +189 -0
- package/docs/SCHEDULING.md +104 -0
- package/docs/SETUP-GOOGLE.md +215 -0
- package/docs/examples/campaign.json +59 -0
- package/docs/examples/draft.md +43 -0
- package/docs/screenshots/overview.png +0 -0
- package/ingest/__pycache__/analyze_ga4.cpython-313.pyc +0 -0
- package/ingest/__pycache__/analyze_gsc.cpython-313.pyc +0 -0
- package/ingest/__pycache__/analyze_metadata.cpython-313.pyc +0 -0
- package/ingest/__pycache__/analyze_trends.cpython-313.pyc +0 -0
- package/ingest/__pycache__/google_auth.cpython-313.pyc +0 -0
- package/ingest/__pycache__/http_util.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_ga4.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_gsc.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_index_status.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_timeseries.cpython-313.pyc +0 -0
- package/ingest/__pycache__/seo_config.cpython-313.pyc +0 -0
- package/ingest/analyze_ga4.py +79 -0
- package/ingest/analyze_gsc.py +136 -0
- package/ingest/analyze_metadata.py +158 -0
- package/ingest/analyze_trends.py +145 -0
- package/ingest/google_auth.py +238 -0
- package/ingest/http_util.py +87 -0
- package/ingest/pull_ga4.py +107 -0
- package/ingest/pull_gsc.py +111 -0
- package/ingest/pull_index_status.py +179 -0
- package/ingest/pull_timeseries.py +130 -0
- package/ingest/seo_config.py +213 -0
- package/n-seo.config.example.json +110 -0
- package/ops/__pycache__/daily.cpython-313.pyc +0 -0
- package/ops/__pycache__/daily_diff.cpython-313.pyc +0 -0
- package/ops/__pycache__/demo_data.cpython-313.pyc +0 -0
- package/ops/__pycache__/doctor.cpython-313.pyc +0 -0
- package/ops/__pycache__/export_static.cpython-313.pyc +0 -0
- package/ops/__pycache__/hn_digest.cpython-313.pyc +0 -0
- package/ops/__pycache__/indexnow.cpython-313.pyc +0 -0
- package/ops/__pycache__/llm.cpython-313.pyc +0 -0
- package/ops/__pycache__/opportunity_scan.cpython-313.pyc +0 -0
- package/ops/__pycache__/publish.cpython-313.pyc +0 -0
- package/ops/__pycache__/reddit_digest.cpython-313.pyc +0 -0
- package/ops/daily.py +250 -0
- package/ops/daily_diff.py +151 -0
- package/ops/demo_data.py +529 -0
- package/ops/doctor.py +266 -0
- package/ops/export_static.py +125 -0
- package/ops/hn_digest.py +169 -0
- package/ops/indexnow.py +107 -0
- package/ops/install-launchd.sh +76 -0
- package/ops/llm.py +139 -0
- package/ops/mcp-smoke-stdio.mjs +61 -0
- package/ops/opportunity_scan.py +185 -0
- package/ops/publish.py +158 -0
- package/ops/reddit_digest.py +168 -0
- package/ops/templates/n-seo-daily.service +11 -0
- package/ops/templates/n-seo-daily.timer +11 -0
- package/ops/templates/n-seo-dashboard.service +15 -0
- package/ops/templates/n-seo.cron +3 -0
- package/ops/templates/n-seo.daily.plist +29 -0
- package/ops/templates/n-seo.dashboard.plist +22 -0
- package/package.json +77 -0
- package/probes/__pycache__/site_probe.cpython-313.pyc +0 -0
- package/probes/site_probe.py +201 -0
- package/public/favicon.svg +6 -0
- package/public/styles.css +632 -0
- package/src/actions.ts +255 -0
- package/src/backlog.ts +197 -0
- package/src/config.ts +220 -0
- package/src/data.ts +895 -0
- package/src/insights.ts +22 -0
- package/src/mcp-stdio.ts +21 -0
- package/src/mcp.ts +490 -0
- package/src/server.tsx +260 -0
- package/src/settings.tsx +329 -0
- package/src/views.tsx +1487 -0
- package/tsconfig.json +15 -0
package/docs/INSTANCE.md
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Running n-seo as an engine + instance
|
|
2
|
+
|
|
3
|
+
n-seo can run two ways. In the simplest, you clone the repo, put your config
|
|
4
|
+
in the checkout and run it there. In the second, the checkout is an **engine**
|
|
5
|
+
you never edit, and everything that is yours — config, queue, content, data,
|
|
6
|
+
logs — lives in a separate **instance** directory that the engine is pointed
|
|
7
|
+
at. This document is about the second way, and about why you would want it.
|
|
8
|
+
|
|
9
|
+
## Why split them
|
|
10
|
+
|
|
11
|
+
- **Upgrades without merges.** Upstream changes touch engine files; your
|
|
12
|
+
files live somewhere else. Upgrading is `git pull` (or `npm update`) and a
|
|
13
|
+
test run. There is nothing to reconcile.
|
|
14
|
+
- **Your work in your own repo.** The queue you curate, the drafts and
|
|
15
|
+
campaigns you write, the daily log that accumulates — those are yours, with
|
|
16
|
+
their own history, in a repo you control. Private if you want.
|
|
17
|
+
- **Several instances, one engine.** A consultancy can run one engine and an
|
|
18
|
+
instance per client. A team can run a shared engine on one machine and
|
|
19
|
+
keep each portfolio separate.
|
|
20
|
+
- **The upgrade path gets exercised.** Because upgrading is cheap, you do it
|
|
21
|
+
routinely, and the engine's `check` run is your regression test against
|
|
22
|
+
your own data.
|
|
23
|
+
|
|
24
|
+
## Who owns what
|
|
25
|
+
|
|
26
|
+
| Engine (the n-seo checkout) | Instance (your directory) |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `src/`, `ingest/`, `ops/`, `probes/`, `public/`, `bin/`, `tests/` | `n-seo.config.json` (or whatever `$N_SEO_CONFIG` names) |
|
|
29
|
+
| `n-seo.config.example.json` | `.env` |
|
|
30
|
+
| `docs/PLAYBOOK.md`, `docs/OPERATING-RULES.md`, `docs/ARCHITECTURE.md` | `config/backlog.json`, `config/insights.json` |
|
|
31
|
+
| | `content/drafts/*.md`, `content/campaigns/*.json` |
|
|
32
|
+
| | `data/` (regenerable), `site/` (static export) |
|
|
33
|
+
| | `docs/daily-log.md`, `docs/reports/` |
|
|
34
|
+
| | the IndexNow key file |
|
|
35
|
+
| | `.mcp.json`, `CLAUDE.md`, `.claude/` (your agent's rules) |
|
|
36
|
+
|
|
37
|
+
The engine resolves the instance from, in order: `--instance <path>` on the
|
|
38
|
+
CLI, then `$N_SEO_INSTANCE`, then the current directory. With none of those
|
|
39
|
+
set and a config file next to the engine's `src/`, you are in in-place mode
|
|
40
|
+
and the engine checkout *is* the instance. Nothing in the engine ever writes
|
|
41
|
+
outside the instance directory.
|
|
42
|
+
|
|
43
|
+
## Three ways to run
|
|
44
|
+
|
|
45
|
+
### A. In place
|
|
46
|
+
|
|
47
|
+
Clone, configure, run. One directory.
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
git clone https://github.com/en-dash-consulting/n-seo && cd n-seo
|
|
51
|
+
npm install
|
|
52
|
+
cp n-seo.config.example.json n-seo.config.json # edit sites, auth
|
|
53
|
+
python3 ops/doctor.py
|
|
54
|
+
npm start
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Upgrade:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
git pull --ff-only && npm ci && npm run check
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Merge conflicts are only possible in files you edited. After 0.1, upstream
|
|
64
|
+
does not change `config/backlog.json`, `config/insights.json` or anything
|
|
65
|
+
under `content/`, so a config-only setup pulls clean. If you edit engine
|
|
66
|
+
files — a rule in `src/actions.ts`, a page in `src/views.tsx` — you are
|
|
67
|
+
forking; add `upstream` as a remote and rebase or merge on your schedule:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
git remote add upstream https://github.com/en-dash-consulting/n-seo
|
|
71
|
+
git fetch upstream && git rebase upstream/main # or merge
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### B. Engine + instance directory
|
|
75
|
+
|
|
76
|
+
Two directories. The engine is a plain checkout; the instance is scaffolded
|
|
77
|
+
by the CLI and holds everything that is yours.
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
git clone https://github.com/en-dash-consulting/n-seo ~/tools/n-seo
|
|
81
|
+
cd ~/tools/n-seo && npm install
|
|
82
|
+
|
|
83
|
+
node ~/tools/n-seo/bin/n-seo.mjs init ~/sites/search-ops
|
|
84
|
+
cd ~/sites/search-ops
|
|
85
|
+
# edit n-seo.config.json, .env
|
|
86
|
+
n-seo doctor # via the .mcp.json/env the scaffold wrote, or:
|
|
87
|
+
node ~/tools/n-seo/bin/n-seo.mjs doctor --instance ~/sites/search-ops
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`n-seo init` writes: `n-seo.config.json` (from the example), empty
|
|
91
|
+
`config/backlog.json` and `config/insights.json`, `content/drafts/README.md`
|
|
92
|
+
and `content/campaigns/README.md`, `.env` (from `.env.example`), a
|
|
93
|
+
`.gitignore` that excludes `data/`, `site/` and `.env`, a `.mcp.json` that
|
|
94
|
+
spawns the engine's MCP server with `N_SEO_INSTANCE` set, and a short README.
|
|
95
|
+
|
|
96
|
+
To make `n-seo` a command, link the engine once:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
cd ~/tools/n-seo && npm link # puts `n-seo` on your PATH
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Daily use, from inside the instance directory (or with `--instance`):
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
n-seo start # dashboard on the config's port
|
|
106
|
+
n-seo daily # the morning run
|
|
107
|
+
n-seo daily --only probe,gsc
|
|
108
|
+
n-seo doctor
|
|
109
|
+
n-seo export # static export → <instance>/site/
|
|
110
|
+
n-seo mcp # stdio MCP server for this instance
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Scheduling: the templates in `ops/templates/` run `ops/daily.py` and
|
|
114
|
+
`src/server.tsx` from the engine checkout. For an instance, change the
|
|
115
|
+
program to the CLI and name the instance, either as an argument:
|
|
116
|
+
|
|
117
|
+
```xml
|
|
118
|
+
<key>ProgramArguments</key>
|
|
119
|
+
<array>
|
|
120
|
+
<string>/usr/bin/env</string>
|
|
121
|
+
<string>node</string>
|
|
122
|
+
<string>/Users/you/tools/n-seo/bin/n-seo.mjs</string>
|
|
123
|
+
<string>daily</string>
|
|
124
|
+
<string>--instance</string>
|
|
125
|
+
<string>/Users/you/sites/search-ops</string>
|
|
126
|
+
</array>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
or as an environment variable in the plist / unit / crontab:
|
|
130
|
+
|
|
131
|
+
```xml
|
|
132
|
+
<key>EnvironmentVariables</key>
|
|
133
|
+
<dict>
|
|
134
|
+
<key>N_SEO_INSTANCE</key>
|
|
135
|
+
<string>/Users/you/sites/search-ops</string>
|
|
136
|
+
</dict>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```cron
|
|
140
|
+
0 7 * * * N_SEO_INSTANCE=/home/you/sites/search-ops /usr/bin/env node /home/you/tools/n-seo/bin/n-seo.mjs daily >> /home/you/sites/search-ops/data/daily-cron.log 2>&1
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Log paths in the templates point at `data/` inside the checkout; in instance
|
|
144
|
+
mode point them at `<instance>/data/` instead. Do the same for the dashboard
|
|
145
|
+
service (`start` instead of `daily`, `KeepAlive` / `Restart=always` as in the
|
|
146
|
+
templates).
|
|
147
|
+
|
|
148
|
+
Upgrade:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
n-seo upgrade
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This runs `git pull --ff-only` in the engine, `npm ci` if the lockfile
|
|
155
|
+
changed, then `npm run check` (typecheck plus both test suites). If the check
|
|
156
|
+
fails it prints the rollback command — `git -C <engine> checkout <previous sha>`
|
|
157
|
+
followed by `npm ci` — and exits non-zero. Nothing in the instance is touched
|
|
158
|
+
by an upgrade.
|
|
159
|
+
|
|
160
|
+
### C. npm dependency
|
|
161
|
+
|
|
162
|
+
The instance is a small npm project that depends on the engine. Nothing to
|
|
163
|
+
clone; upgrades are `npm update`.
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
mkdir ~/sites/search-ops && cd ~/sites/search-ops
|
|
167
|
+
npm init -y
|
|
168
|
+
npm install github:en-dash-consulting/n-seo # or `n-seo@^0.1` once published
|
|
169
|
+
npx n-seo init .
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`package.json` scripts make the commands local:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"scripts": {
|
|
177
|
+
"start": "n-seo start",
|
|
178
|
+
"daily": "n-seo daily",
|
|
179
|
+
"doctor": "n-seo doctor",
|
|
180
|
+
"export": "n-seo export",
|
|
181
|
+
"upgrade": "npm update n-seo && n-seo check"
|
|
182
|
+
},
|
|
183
|
+
"dependencies": { "n-seo": "github:en-dash-consulting/n-seo" }
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The engine lives in `node_modules/n-seo`; the instance is the current
|
|
188
|
+
directory. `n-seo upgrade` recognizes an npm install and tells you to run
|
|
189
|
+
`npm update n-seo` instead of pulling. Pin with a version or a commit
|
|
190
|
+
(`github:en-dash-consulting/n-seo#<sha>`) when you need to.
|
|
191
|
+
|
|
192
|
+
Python is still required on the machine — the engine's ingest scripts run
|
|
193
|
+
with `python3` from the package directory.
|
|
194
|
+
|
|
195
|
+
## The instance repo
|
|
196
|
+
|
|
197
|
+
A typical instance, committed to its own git repo:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
search-ops/
|
|
201
|
+
n-seo.config.json sites, auth, modules, hooks ← commit
|
|
202
|
+
.env Reddit creds, MCP token ← never commit
|
|
203
|
+
config/backlog.json your curated queue ← commit
|
|
204
|
+
config/insights.json your narrative briefing ← commit
|
|
205
|
+
content/drafts/ distribution drafts (markdown) ← commit
|
|
206
|
+
content/campaigns/ outreach campaigns (json) ← commit
|
|
207
|
+
docs/daily-log.md appended by every run ← commit
|
|
208
|
+
docs/reports/ analyze_gsc / analyze_ga4 output ← commit
|
|
209
|
+
data/ snapshots, regenerable ← ignore
|
|
210
|
+
site/ static export ← ignore (or commit if you deploy it from git)
|
|
211
|
+
indexnow.key public by design ← commit
|
|
212
|
+
ops/ your own scripts, run by hooks ← commit
|
|
213
|
+
.mcp.json, CLAUDE.md, .claude/ ← commit
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`.gitignore` written by `n-seo init`:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
data/
|
|
220
|
+
site/
|
|
221
|
+
.env
|
|
222
|
+
__pycache__/
|
|
223
|
+
node_modules/
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
If `modules.gitAutoCommit` is on, the daily run commits `docs/daily-log.md`,
|
|
227
|
+
`docs/reports/` and `site/` (when tracked) in the **instance** repo and pushes
|
|
228
|
+
if it has a remote. The engine checkout is never committed to by the run.
|
|
229
|
+
|
|
230
|
+
## Hooks: your scripts in the daily run
|
|
231
|
+
|
|
232
|
+
`hooks` in the config attach shell commands to the run. Each command runs
|
|
233
|
+
with the instance directory as its working directory and these variables
|
|
234
|
+
set: `N_SEO_ROOT` (the engine), `N_SEO_INSTANCE`, and for `afterStep`,
|
|
235
|
+
`N_SEO_STEP`. Hook output goes to `data/daily-ops.log` like a step; a
|
|
236
|
+
failing hook is recorded in `data/last-run.json` and never aborts the run.
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
"hooks": {
|
|
240
|
+
"beforeRun": [],
|
|
241
|
+
"afterStep": {
|
|
242
|
+
"gsc": ["python3 ops/flag_new_queries.py"],
|
|
243
|
+
"daily-diff": ["git add docs/daily-log.md && git commit -qm \"daily log $(date +%F)\" || true", "git push -q origin main || true"]
|
|
244
|
+
},
|
|
245
|
+
"afterRun": [
|
|
246
|
+
"rsync -a --delete site/ deploy@mirror.example.com:/srv/search-ops/",
|
|
247
|
+
"CLOUDSDK_CONFIG=$HOME/.config/my-gcloud gcloud storage rsync site gs://my-search-ops-mirror --recursive --delete-unmatched-destination-objects"
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Three patterns that cover most needs:
|
|
253
|
+
|
|
254
|
+
1. **Publish a mirror after the run.** With `modules.staticExport` on, the
|
|
255
|
+
run leaves `site/` in the instance; an `afterRun` `rsync` or
|
|
256
|
+
`gcloud storage rsync` puts it behind whatever auth you already have.
|
|
257
|
+
Choose one of the two lines above, not both.
|
|
258
|
+
2. **Push the instance repo as soon as the log is written.** The
|
|
259
|
+
`afterStep.daily-diff` pair above commits and pushes the day's entry
|
|
260
|
+
before the slower digest steps run. Leave `gitAutoCommit` off if you do
|
|
261
|
+
this, or you will get two commits a day.
|
|
262
|
+
3. **Run your own analysis on fresh data.** Anything under the instance's
|
|
263
|
+
own `ops/` can read `data/` (the layout is in `docs/ARCHITECTURE.md`)
|
|
264
|
+
and write wherever you like. `afterStep.gsc` runs right after the Search
|
|
265
|
+
Console pull lands, before the audit and the scan.
|
|
266
|
+
|
|
267
|
+
Step names, for `afterStep`: `probe`, `gsc`, `ga4`, `timeseries`,
|
|
268
|
+
`metadata-audit`, `index-status`, `opportunity-scan`, `daily-diff`,
|
|
269
|
+
`hn-digest`, `reddit-digest`, `static-export`. A hook on a step whose module
|
|
270
|
+
is off never runs.
|
|
271
|
+
|
|
272
|
+
## Extra Search Console properties
|
|
273
|
+
|
|
274
|
+
A host can be verified in Search Console as both a domain property and a
|
|
275
|
+
URL-prefix property, and the two report differently. The sites list takes one
|
|
276
|
+
`gscProperty` per site; anything else you want pulled for history goes in
|
|
277
|
+
`gscExtraProperties`:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
"gscExtraProperties": ["https://example.com/"]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
They land in `data/gsc/<slug>/` with the same files as any property. A
|
|
284
|
+
url-prefix property gets a `-urlprefix` suffix (`https://example.com/` →
|
|
285
|
+
`data/gsc/example.com-urlprefix/`), so it never collides with the domain
|
|
286
|
+
property's directory. Nothing in the UI reads extras — they are for your own
|
|
287
|
+
scripts and for comparison.
|
|
288
|
+
|
|
289
|
+
## Your agent, in the instance
|
|
290
|
+
|
|
291
|
+
`n-seo init` writes a `.mcp.json` that spawns the engine's stdio MCP server
|
|
292
|
+
with `N_SEO_INSTANCE` pointing at the instance, so opening the instance
|
|
293
|
+
directory in Claude Code gives the agent the instance's queue and data:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"mcpServers": {
|
|
298
|
+
"n-seo": {
|
|
299
|
+
"command": "node",
|
|
300
|
+
"args": ["/Users/you/tools/n-seo/bin/n-seo.mjs", "mcp"],
|
|
301
|
+
"env": { "N_SEO_INSTANCE": "/Users/you/sites/search-ops" }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`CLAUDE.md` and `.claude/skills/` in the instance are yours: put the
|
|
308
|
+
operating rules that are specific to your sites there (approval gates,
|
|
309
|
+
freeze dates, who may post where). The engine's generic rules stay in
|
|
310
|
+
`docs/OPERATING-RULES.md` and are also served as the MCP resource
|
|
311
|
+
`seo://docs/operating-rules`. The `engine_info` tool reports the engine
|
|
312
|
+
version, commit, mode and both paths, so an agent can tell which engine it
|
|
313
|
+
is talking to.
|
|
314
|
+
|
|
315
|
+
## Testing the upgrade path as routine
|
|
316
|
+
|
|
317
|
+
The point of the split is that upgrading is boring. Make it a habit:
|
|
318
|
+
|
|
319
|
+
- **Weekly, automatically.** A `beforeRun` hook on Mondays:
|
|
320
|
+
```json
|
|
321
|
+
"hooks": { "beforeRun": ["[ \"$(date +%u)\" = 1 ] && n-seo upgrade || true"] }
|
|
322
|
+
```
|
|
323
|
+
or a separate scheduled job (a second launchd plist or cron line) that
|
|
324
|
+
runs `n-seo upgrade` an hour before the daily run.
|
|
325
|
+
- **`npm run check` is the gate.** `n-seo upgrade` refuses to leave the
|
|
326
|
+
engine on a commit whose typecheck or tests fail. Your `data/` is not
|
|
327
|
+
used by the tests (they run on a sandboxed copy with demo data), so a
|
|
328
|
+
failing check means the engine is broken, not your instance.
|
|
329
|
+
- **Rollback is one command**, printed on failure:
|
|
330
|
+
```sh
|
|
331
|
+
git -C ~/tools/n-seo checkout <previous-sha> && (cd ~/tools/n-seo && npm ci)
|
|
332
|
+
```
|
|
333
|
+
- **Pin when you need to.** `git -C ~/tools/n-seo checkout v0.1.0` (or a
|
|
334
|
+
sha) holds the engine; `n-seo upgrade` then fast-forwards only when you
|
|
335
|
+
move back to `main`. In npm mode, pin the version in `package.json`.
|
|
336
|
+
- **Watch the Settings page.** It shows the engine version and commit the
|
|
337
|
+
dashboard is running, which is how you notice a scheduled upgrade
|
|
338
|
+
silently failed. `n-seo doctor` prints the same block.
|
|
339
|
+
|
|
340
|
+
## FAQ
|
|
341
|
+
|
|
342
|
+
**Can I change a rule (a threshold in the action engine, a probe check)?**
|
|
343
|
+
Not from an instance — the rules are engine code. Either run in-place mode
|
|
344
|
+
as a fork (mode A with an `upstream` remote) or send the change upstream as a
|
|
345
|
+
pull request, which is the better outcome if the rule is generally right.
|
|
346
|
+
Instance-level customization is what `config/backlog.json`, `watchPages`,
|
|
347
|
+
`conversions`, the module fields and hooks are for.
|
|
348
|
+
|
|
349
|
+
**Where do custom dashboard pages go?**
|
|
350
|
+
Instance mode does not support them yet. Fork. If a page would be useful to
|
|
351
|
+
others, propose it.
|
|
352
|
+
|
|
353
|
+
**What happens when upstream changes a data file format?**
|
|
354
|
+
The engine's readers, writers and `demo_data.py` move together in one
|
|
355
|
+
commit, and `data/` is regenerable: the next daily run rewrites every file.
|
|
356
|
+
If a format change ever needs a manual step, the changelog says so and
|
|
357
|
+
`n-seo upgrade` prints it.
|
|
358
|
+
|
|
359
|
+
**Can two instances share one `data/`?**
|
|
360
|
+
No. `data/` belongs to an instance. Two instances with the same sites will
|
|
361
|
+
pull the same data twice; that is fine within Google's quotas.
|
|
362
|
+
|
|
363
|
+
**Does the engine ever write to the instance's `config/backlog.json`?**
|
|
364
|
+
Only through the dashboard's accept / mark-watching / retire buttons, which
|
|
365
|
+
you click. The daily run never writes the queue.
|
package/docs/MCP.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# MCP server — the same data, for agents
|
|
2
|
+
|
|
3
|
+
`src/mcp.ts` exposes the control plane over the Model Context Protocol so an
|
|
4
|
+
AI agent can read the queue and the metrics directly instead of scraping the
|
|
5
|
+
dashboard. It is **read-only by design**: the queue is edited through the
|
|
6
|
+
dashboard's accept/watch/retire flow, so nothing here writes anything. Every
|
|
7
|
+
tool is annotated `readOnlyHint`.
|
|
8
|
+
|
|
9
|
+
## Two transports
|
|
10
|
+
|
|
11
|
+
### stdio (default — no secret)
|
|
12
|
+
|
|
13
|
+
The client spawns the server as a child process and talks over stdin/stdout.
|
|
14
|
+
There is no network listener and no token: the OS process boundary is the
|
|
15
|
+
authentication, and the server can only read what you can.
|
|
16
|
+
|
|
17
|
+
`.mcp.json` in the repo root registers it for **Claude Code**, so opening the
|
|
18
|
+
repo in Claude Code is all the setup there is.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm run mcp # run it by hand (protocol on stdout, diagnostics on stderr)
|
|
22
|
+
npm run mcp:smoke # list tools and call a few against your data
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Claude Desktop** — add to its MCP settings, with `cwd` set to your checkout:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"n-seo": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["tsx", "src/mcp-stdio.ts"],
|
|
33
|
+
"cwd": "/path/to/n-seo"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### HTTP (for clients that cannot spawn a process)
|
|
40
|
+
|
|
41
|
+
`POST /mcp` on the dashboard, streamable HTTP, stateless — a fresh server per
|
|
42
|
+
request, so no session state crosses callers. It requires a bearer token,
|
|
43
|
+
read from `SEO_MCP_TOKEN` (env or `.env`), else from
|
|
44
|
+
`~/.config/n-seo/mcp-token` (chmod 600).
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
curl -X POST http://localhost:4600/mcp \
|
|
48
|
+
-H "Authorization: Bearer $(cat ~/.config/n-seo/mcp-token)" \
|
|
49
|
+
-H 'Content-Type: application/json' \
|
|
50
|
+
-H 'Accept: application/json, text/event-stream' \
|
|
51
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**With no token set the endpoint returns 503 rather than serving.** It fails
|
|
55
|
+
closed so that your search data is never readable by accident: the dashboard
|
|
56
|
+
binds `127.0.0.1` by default, but `SEO_HOST=0.0.0.0` (for a tunnel or a
|
|
57
|
+
container) exposes it to whatever can reach that interface. Rotate by writing a new
|
|
58
|
+
value to the token file and restarting the dashboard.
|
|
59
|
+
|
|
60
|
+
## Why local-only
|
|
61
|
+
|
|
62
|
+
All the real data lives in `data/` on the machine that runs the daily job. An
|
|
63
|
+
MCP server therefore only makes sense next to those files. To reach it from
|
|
64
|
+
another device, tunnel to the dashboard port (Tailscale, `cloudflared`,
|
|
65
|
+
an SSH tunnel) rather than exposing it — the bearer token is the only thing
|
|
66
|
+
in front of it.
|
|
67
|
+
|
|
68
|
+
## Tools
|
|
69
|
+
|
|
70
|
+
| Tool | Returns |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `list_actions` | The queue, ranked by impact per effort. Filter by `status` (active / watching / all), `host`, `tag`; `limit` |
|
|
73
|
+
| `get_action` | One action in full (spec and success criteria), matched by title substring |
|
|
74
|
+
| `list_sites` | Every configured site with 16-month GSC totals, 90-day session trend, probe status |
|
|
75
|
+
| `site_report` | One site: totals, traffic mix (search / AI assistants / other), trend, landing pages, probe findings |
|
|
76
|
+
| `top_queries` | GSC queries for a site, `window` 90d or 16mo |
|
|
77
|
+
| `striking_distance` | Queries at position 5–15 with meaningful impressions (90d) |
|
|
78
|
+
| `ctr_gaps` | Queries whose CTR trails what their position should earn (90d) |
|
|
79
|
+
| `metadata_audit` | Per-page title/description findings from the daily crawl |
|
|
80
|
+
| `trends_timeseries` | Daily clicks and sessions for a site over the last N days |
|
|
81
|
+
| `ops_status` | Did the last run succeed, how stale is each dataset, current probe results — check before trusting numbers |
|
|
82
|
+
| `daily_log` | The last N entries of `docs/daily-log.md` |
|
|
83
|
+
| `opportunity_proposals` | Rising-query candidates, machine proposals awaiting accept, verdicts on watching items |
|
|
84
|
+
| `conversions_status` | Whether conversion events are instrumented and their 28-day counts by source |
|
|
85
|
+
| `campaigns` | Outreach campaigns from `content/campaigns/` — targets, plan, template ids |
|
|
86
|
+
| `settings` | The effective config: sites, module switches, auth mode (never the key) |
|
|
87
|
+
|
|
88
|
+
Resources: `seo://docs/playbook`, `seo://docs/daily-log`,
|
|
89
|
+
`seo://docs/operating-rules`.
|
|
90
|
+
|
|
91
|
+
## Example prompts
|
|
92
|
+
|
|
93
|
+
- "What should I do first this week? Use `list_actions` and explain the top
|
|
94
|
+
three in terms of the evidence."
|
|
95
|
+
- "Which pages on example.com have CTR gaps, and what would you retitle them
|
|
96
|
+
to? Respect the 28-day freeze on anything in the watching list."
|
|
97
|
+
- "Is the data fresh? Check `ops_status` before answering anything else."
|
|
98
|
+
- "Summarize the last five daily-log entries — what regressed, what moved."
|
|
99
|
+
- "Read the opportunity proposals and tell me which ones are worth accepting
|
|
100
|
+
and why. Don't add anything to the queue."
|
|
101
|
+
|
|
102
|
+
The rules in `CLAUDE.md` apply to an agent using these tools: it proposes,
|
|
103
|
+
you accept; it never drafts community comments; it reads outputs rather than
|
|
104
|
+
re-running pulls.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Operating rules
|
|
2
|
+
|
|
3
|
+
These are the rules the tooling encodes and the docs keep repeating. Each one
|
|
4
|
+
exists because the opposite was tried and cost something. An agent working in
|
|
5
|
+
this repo is bound by them too (see `CLAUDE.md`).
|
|
6
|
+
|
|
7
|
+
## The queue is the single source of "what's next"
|
|
8
|
+
|
|
9
|
+
`config/backlog.json` merged with the data-derived rules, shown on `/actions`
|
|
10
|
+
and via the MCP `list_actions` tool. New findings become entries with
|
|
11
|
+
evidence and a spec. Shipped work gets a `watching` note. Corrections from the
|
|
12
|
+
owner are applied immediately.
|
|
13
|
+
|
|
14
|
+
**Why:** the moment "what's next" lives in three places — a doc, a chat, a
|
|
15
|
+
head — the data stops deciding and recency does.
|
|
16
|
+
|
|
17
|
+
## Shipped work becomes *watching*; it is never deleted
|
|
18
|
+
|
|
19
|
+
When an action ships, set `watching` to a dated note ("title trimmed 3/14,
|
|
20
|
+
frozen until 4/11, watching CTR") and add the page to `shippedWatch` so the
|
|
21
|
+
data-derived cards for that page show as watching too.
|
|
22
|
+
|
|
23
|
+
**Why:** the point of the change was to move a number. If the item disappears
|
|
24
|
+
you never learn whether it did, and the same page gets "fixed" again in six
|
|
25
|
+
weeks by someone reading the same CTR gap.
|
|
26
|
+
|
|
27
|
+
## Decisions ride the 90-day window
|
|
28
|
+
|
|
29
|
+
Rules, audits and rankings read the `*_90d.json` files and the 90-day
|
|
30
|
+
metadata audit. The 16-month pull is for totals and history only.
|
|
31
|
+
|
|
32
|
+
**Why:** a page fixed last week must stop being accused within a season.
|
|
33
|
+
Sixteen-month aggregates keep a dead query cluster looking alive and a fixed
|
|
34
|
+
page looking broken.
|
|
35
|
+
|
|
36
|
+
## Impact numbers order the queue. They are not forecasts
|
|
37
|
+
|
|
38
|
+
Every action carries an impact estimate (clicks per month). It exists so
|
|
39
|
+
that impact ÷ effort can sort the list. It is not a prediction, and it is
|
|
40
|
+
never reported as an expected result. When observed data exists for a
|
|
41
|
+
shipped item, recalibrate against it.
|
|
42
|
+
|
|
43
|
+
**Why:** the estimates are rough by construction (industry CTR curves ×
|
|
44
|
+
impressions). Treating them as promises leads to disappointment on the
|
|
45
|
+
misses and over-investment on the flukes.
|
|
46
|
+
|
|
47
|
+
## Title-change freeze: 28 days
|
|
48
|
+
|
|
49
|
+
After rewriting a page's title or description, do not touch that page's
|
|
50
|
+
metadata again for 28 days. Measure, then move.
|
|
51
|
+
|
|
52
|
+
**Why:** search engines re-evaluate a page after a title change and need
|
|
53
|
+
weeks of impression data to settle. Iterating inside that window destroys
|
|
54
|
+
the measurement and, at scale, reads as manipulation.
|
|
55
|
+
|
|
56
|
+
## Stagger metadata batches: ≈8 per week across all sites
|
|
57
|
+
|
|
58
|
+
Even when the audit lists 25 pages with title problems, ship about eight a
|
|
59
|
+
week.
|
|
60
|
+
|
|
61
|
+
**Why:** a burst of simultaneous title changes across a site is a churn
|
|
62
|
+
pattern that invites re-evaluation of the whole site rather than the pages
|
|
63
|
+
you changed. Batching also keeps the 28-day freezes from all expiring on the
|
|
64
|
+
same day, which is what makes weekly measurement readable.
|
|
65
|
+
|
|
66
|
+
## Proposals never self-promote into the queue
|
|
67
|
+
|
|
68
|
+
The opportunity scan writes `data/opportunity-proposals.json`. Its proposals
|
|
69
|
+
show on `/actions` as *proposed* until you click accept (or add them to the
|
|
70
|
+
backlog by hand). Its verdicts on watching items (succeeded / failed /
|
|
71
|
+
keep-watching) are review triggers, not actions.
|
|
72
|
+
|
|
73
|
+
**Why:** a machine adding items to its own to-do list has no brake. Keeping
|
|
74
|
+
the accept step human keeps the queue something you believe in.
|
|
75
|
+
|
|
76
|
+
## Participation is human
|
|
77
|
+
|
|
78
|
+
The Hacker News and Reddit modules find threads where your first-hand
|
|
79
|
+
experience applies and write a briefing: what the piece says, what the
|
|
80
|
+
thread is debating, where you genuinely connect. They never draft the
|
|
81
|
+
comment, and neither should any agent using this repo. Sustainable pace is
|
|
82
|
+
two to four genuine comments a day.
|
|
83
|
+
|
|
84
|
+
**Why:** generated participation is detectable, against the rules of the
|
|
85
|
+
communities involved, and a ban destroys the account history that makes
|
|
86
|
+
your own launches land later. The briefing gets you to the thread fast; the
|
|
87
|
+
words are what make it worth being there.
|
|
88
|
+
|
|
89
|
+
## Site changes ship as branches and PRs
|
|
90
|
+
|
|
91
|
+
Changes to your sites go into an `seo-*` branch off freshly pulled main in
|
|
92
|
+
the site's own repo, pushed, and reviewed as a PR. Never direct to main. If a
|
|
93
|
+
checkout has uncommitted work, use a git worktree rather than touching it.
|
|
94
|
+
|
|
95
|
+
**Why:** the change log is how movement gets attributed. A PR with the queue
|
|
96
|
+
item's evidence in its description is the record that lets the next daily
|
|
97
|
+
run say "this moved because of that."
|
|
98
|
+
|
|
99
|
+
## Read the outputs; don't re-derive them
|
|
100
|
+
|
|
101
|
+
The daily run pulls, probes, audits and logs. To answer "how are we doing,"
|
|
102
|
+
read `docs/daily-log.md`, `data/last-run.json` and the dashboard — do not
|
|
103
|
+
re-run the pipeline, re-crawl the sites, or recompute the trends.
|
|
104
|
+
|
|
105
|
+
**Why:** each re-run spends API quota and time to reproduce a file that
|
|
106
|
+
already exists, and it invites two versions of the truth.
|