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.
Files changed (88) hide show
  1. package/.env.example +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +184 -0
  4. package/bin/n-seo.mjs +310 -0
  5. package/docs/ADDING-A-SITE.md +82 -0
  6. package/docs/ARCHITECTURE.md +213 -0
  7. package/docs/DEPLOY.md +300 -0
  8. package/docs/FAQ.md +93 -0
  9. package/docs/INSTANCE.md +365 -0
  10. package/docs/MCP.md +104 -0
  11. package/docs/OPERATING-RULES.md +106 -0
  12. package/docs/PLAYBOOK.md +122 -0
  13. package/docs/PRD.md +249 -0
  14. package/docs/RELEASING.md +189 -0
  15. package/docs/SCHEDULING.md +104 -0
  16. package/docs/SETUP-GOOGLE.md +215 -0
  17. package/docs/examples/campaign.json +59 -0
  18. package/docs/examples/draft.md +43 -0
  19. package/docs/screenshots/overview.png +0 -0
  20. package/ingest/__pycache__/analyze_ga4.cpython-313.pyc +0 -0
  21. package/ingest/__pycache__/analyze_gsc.cpython-313.pyc +0 -0
  22. package/ingest/__pycache__/analyze_metadata.cpython-313.pyc +0 -0
  23. package/ingest/__pycache__/analyze_trends.cpython-313.pyc +0 -0
  24. package/ingest/__pycache__/google_auth.cpython-313.pyc +0 -0
  25. package/ingest/__pycache__/http_util.cpython-313.pyc +0 -0
  26. package/ingest/__pycache__/pull_ga4.cpython-313.pyc +0 -0
  27. package/ingest/__pycache__/pull_gsc.cpython-313.pyc +0 -0
  28. package/ingest/__pycache__/pull_index_status.cpython-313.pyc +0 -0
  29. package/ingest/__pycache__/pull_timeseries.cpython-313.pyc +0 -0
  30. package/ingest/__pycache__/seo_config.cpython-313.pyc +0 -0
  31. package/ingest/analyze_ga4.py +79 -0
  32. package/ingest/analyze_gsc.py +136 -0
  33. package/ingest/analyze_metadata.py +158 -0
  34. package/ingest/analyze_trends.py +145 -0
  35. package/ingest/google_auth.py +238 -0
  36. package/ingest/http_util.py +87 -0
  37. package/ingest/pull_ga4.py +107 -0
  38. package/ingest/pull_gsc.py +111 -0
  39. package/ingest/pull_index_status.py +179 -0
  40. package/ingest/pull_timeseries.py +130 -0
  41. package/ingest/seo_config.py +213 -0
  42. package/n-seo.config.example.json +110 -0
  43. package/ops/__pycache__/daily.cpython-313.pyc +0 -0
  44. package/ops/__pycache__/daily_diff.cpython-313.pyc +0 -0
  45. package/ops/__pycache__/demo_data.cpython-313.pyc +0 -0
  46. package/ops/__pycache__/doctor.cpython-313.pyc +0 -0
  47. package/ops/__pycache__/export_static.cpython-313.pyc +0 -0
  48. package/ops/__pycache__/hn_digest.cpython-313.pyc +0 -0
  49. package/ops/__pycache__/indexnow.cpython-313.pyc +0 -0
  50. package/ops/__pycache__/llm.cpython-313.pyc +0 -0
  51. package/ops/__pycache__/opportunity_scan.cpython-313.pyc +0 -0
  52. package/ops/__pycache__/publish.cpython-313.pyc +0 -0
  53. package/ops/__pycache__/reddit_digest.cpython-313.pyc +0 -0
  54. package/ops/daily.py +250 -0
  55. package/ops/daily_diff.py +151 -0
  56. package/ops/demo_data.py +529 -0
  57. package/ops/doctor.py +266 -0
  58. package/ops/export_static.py +125 -0
  59. package/ops/hn_digest.py +169 -0
  60. package/ops/indexnow.py +107 -0
  61. package/ops/install-launchd.sh +76 -0
  62. package/ops/llm.py +139 -0
  63. package/ops/mcp-smoke-stdio.mjs +61 -0
  64. package/ops/opportunity_scan.py +185 -0
  65. package/ops/publish.py +158 -0
  66. package/ops/reddit_digest.py +168 -0
  67. package/ops/templates/n-seo-daily.service +11 -0
  68. package/ops/templates/n-seo-daily.timer +11 -0
  69. package/ops/templates/n-seo-dashboard.service +15 -0
  70. package/ops/templates/n-seo.cron +3 -0
  71. package/ops/templates/n-seo.daily.plist +29 -0
  72. package/ops/templates/n-seo.dashboard.plist +22 -0
  73. package/package.json +77 -0
  74. package/probes/__pycache__/site_probe.cpython-313.pyc +0 -0
  75. package/probes/site_probe.py +201 -0
  76. package/public/favicon.svg +6 -0
  77. package/public/styles.css +632 -0
  78. package/src/actions.ts +255 -0
  79. package/src/backlog.ts +197 -0
  80. package/src/config.ts +220 -0
  81. package/src/data.ts +895 -0
  82. package/src/insights.ts +22 -0
  83. package/src/mcp-stdio.ts +21 -0
  84. package/src/mcp.ts +490 -0
  85. package/src/server.tsx +260 -0
  86. package/src/settings.tsx +329 -0
  87. package/src/views.tsx +1487 -0
  88. package/tsconfig.json +15 -0
@@ -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.