n-seo 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/n-seo-add-site/SKILL.md +125 -0
- package/.claude/skills/n-seo-deploy/SKILL.md +171 -0
- package/.claude/skills/n-seo-review/SKILL.md +106 -0
- package/.claude/skills/n-seo-setup/SKILL.md +139 -0
- package/.claude/skills/n-seo-ship/SKILL.md +122 -0
- package/.claude/skills/n-seo-triage/SKILL.md +90 -0
- package/.claude/skills/orient/SKILL.md +54 -0
- package/README.md +93 -15
- package/bin/n-seo.mjs +163 -6
- package/docs/ADDING-A-SITE.md +4 -0
- package/docs/DEPLOY.md +4 -0
- package/docs/FAQ.md +32 -3
- package/docs/INSTANCE.md +54 -0
- package/docs/PRD.md +23 -5
- package/docs/SCHEDULING.md +4 -0
- package/docs/SETUP-GOOGLE.md +7 -0
- package/ingest/__pycache__/analyze_metadata.cpython-312.pyc +0 -0
- package/ingest/__pycache__/google_auth.cpython-312.pyc +0 -0
- package/ingest/__pycache__/http_util.cpython-312.pyc +0 -0
- package/ingest/__pycache__/seo_config.cpython-312.pyc +0 -0
- package/ops/__pycache__/daily.cpython-312.pyc +0 -0
- package/ops/__pycache__/daily_diff.cpython-312.pyc +0 -0
- package/ops/__pycache__/demo_data.cpython-312.pyc +0 -0
- package/ops/__pycache__/export_static.cpython-312.pyc +0 -0
- package/ops/__pycache__/llm.cpython-312.pyc +0 -0
- package/ops/__pycache__/publish.cpython-312.pyc +0 -0
- package/ops/demo_data.py +6 -0
- package/ops/doctor.py +9 -0
- package/ops/export_static.py +5 -0
- package/package.json +10 -2
- package/probes/__pycache__/site_probe.cpython-312.pyc +0 -0
- package/public/fonts/OFL.txt +105 -0
- package/public/fonts/montserrat-latin-var.woff2 +0 -0
- package/public/styles.css +18 -1
- package/src/server.tsx +22 -1
- package/src/views.tsx +70 -6
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-seo-triage
|
|
3
|
+
description: Answer "what should I work on today" from an n-seo instance — the ranked queue, the last run, and the scan proposals, with the evidence and the operating rules applied. Use for any status or what-next question.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Triage — what to do today
|
|
7
|
+
|
|
8
|
+
Read the system's outputs. **Do not re-run pulls, re-crawl sites, or
|
|
9
|
+
recompute trends to answer a status question** — the daily run already
|
|
10
|
+
produced all of it, and a re-run spends quota to reproduce a file that exists.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
E=<engine checkout>/bin/n-seo.mjs
|
|
14
|
+
I=<instance dir>
|
|
15
|
+
PORT=$(python3 -c "import json;print(json.load(open('$I/n-seo.config.json'))['port'])" 2>/dev/null || echo 4600)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 1. Trust the data before you use it
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
cat "$I/data/last-run.json" # ts, failures, per-step timing
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or the MCP `ops_status` tool, which adds dataset staleness and probe results.
|
|
25
|
+
If `failures` is non-empty, **say so first** — the numbers below are stale in
|
|
26
|
+
whatever the failing step feeds. `data/daily-ops.log` has the reason.
|
|
27
|
+
|
|
28
|
+
## 2. Read the queue
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
curl -s "http://localhost:$PORT/api/actions"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or MCP `list_actions` (`status: active`, optional `host`, `tag`, `limit`),
|
|
35
|
+
which returns the same ranked list. If the dashboard is not running, start it
|
|
36
|
+
(`node $E start --instance "$I"`) or read `config/backlog.json` plus the
|
|
37
|
+
rule-derived cards from the MCP server.
|
|
38
|
+
|
|
39
|
+
Each action carries `impact`, `effort`, `why` (the evidence), `how`, `spec`,
|
|
40
|
+
`tag`, and `watching` when it has shipped. Ranking is impact ÷ effort weight.
|
|
41
|
+
|
|
42
|
+
## 3. Read what happened lately
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
tail -30 "$I/docs/daily-log.md" # or MCP daily_log
|
|
46
|
+
cat "$I/data/opportunity-proposals.json" # or MCP opportunity_proposals
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The log gives probe health, the watched pages' numbers, the conversions line
|
|
50
|
+
and cross-referrals. The proposals file gives rising queries no card covers,
|
|
51
|
+
machine proposals awaiting accept, and verdicts on watching items.
|
|
52
|
+
|
|
53
|
+
## 4. Apply the rules before recommending
|
|
54
|
+
|
|
55
|
+
- **28-day freeze.** A page whose title or description changed inside 28 days
|
|
56
|
+
is off limits for another metadata change. Check `shippedWatch` in
|
|
57
|
+
`config/backlog.json` and the dated `watching` notes for the page.
|
|
58
|
+
- **≈8 metadata changes a week across all sites.** Count what has already
|
|
59
|
+
shipped this week from the recent `watching` notes before proposing more.
|
|
60
|
+
If the audit lists twenty, recommend the top few, not the list.
|
|
61
|
+
- **Impact orders, it does not forecast.** Never present an impact number as
|
|
62
|
+
an expected result. "Ranked highest" not "will earn 60 clicks."
|
|
63
|
+
- **Decisions ride the 90 days.** Cite the 90-day figures the cards use, not
|
|
64
|
+
16-month totals.
|
|
65
|
+
- **Proposals are not queue items** until accepted.
|
|
66
|
+
|
|
67
|
+
## 5. Answer
|
|
68
|
+
|
|
69
|
+
Short, ranked, evidence attached. For each recommendation:
|
|
70
|
+
|
|
71
|
+
- the move, in one line;
|
|
72
|
+
- the numbers behind it, from the card's `why`;
|
|
73
|
+
- why now (freeze expired, riser, regression, seasonal window);
|
|
74
|
+
- what it costs (`effort`).
|
|
75
|
+
|
|
76
|
+
Then, separately and briefly:
|
|
77
|
+
|
|
78
|
+
- **Blocked or broken** — failing steps, probe regressions, index problems.
|
|
79
|
+
- **Waiting on the owner** — scan proposals to accept or ignore, drafts whose
|
|
80
|
+
status contains `approval`.
|
|
81
|
+
- **Watching** — shipped work whose window is closing, and what the data says
|
|
82
|
+
so far. Verdicts of *succeeded* or *failed* are review triggers; hand them
|
|
83
|
+
to `/n-seo-review` rather than acting on them here.
|
|
84
|
+
|
|
85
|
+
## Stay inside the lines
|
|
86
|
+
|
|
87
|
+
- Do not write to `config/backlog.json` here. Triage reads.
|
|
88
|
+
- Do not draft comments for Hacker News or Reddit. The digests brief; the
|
|
89
|
+
human writes. Point at the briefing and stop.
|
|
90
|
+
- Do not enable modules or change settings as part of answering.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orient
|
|
3
|
+
description: Fast, cheap orientation for a fresh session on this n-seo install. Use FIRST in any new session here — reads current state from the running system instead of re-deriving it from raw data.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Orient — get current in ~4 reads, not 40
|
|
7
|
+
|
|
8
|
+
The system is self-updating; your job is to read its outputs, never to rebuild
|
|
9
|
+
its conclusions. Procedure, in order — stop when you have what the task needs:
|
|
10
|
+
|
|
11
|
+
1. **The queue is the truth for "what's next"**:
|
|
12
|
+
`curl -s http://localhost:4600/api/actions` (the port is `port` in
|
|
13
|
+
`n-seo.config.json`; the MCP `list_actions` tool returns the same).
|
|
14
|
+
Every active and watching action with evidence and spec.
|
|
15
|
+
2. **What happened lately**: the last entry of `docs/daily-log.md` (probe
|
|
16
|
+
health, watched-page numbers, conversions line) and `data/last-run.json`
|
|
17
|
+
(did the last daily run succeed, which steps failed).
|
|
18
|
+
3. **Machine-proposed work awaiting the owner**:
|
|
19
|
+
`data/opportunity-proposals.json` — proposals and verdicts from the scan.
|
|
20
|
+
4. **Recent human decisions**: `git log --oneline -10` in this repo.
|
|
21
|
+
|
|
22
|
+
## Do NOT (wastes tokens, re-derives what the batch already did)
|
|
23
|
+
|
|
24
|
+
- Do not re-run pulls, audits, or trend analysis to answer status questions —
|
|
25
|
+
`ops/daily.py` refreshes everything on its schedule; its outputs are in
|
|
26
|
+
`data/` and `docs/daily-log.md`.
|
|
27
|
+
- Do not re-audit sites or re-crawl pages the probe already covers.
|
|
28
|
+
- Do not summarize the architecture from source — `docs/ARCHITECTURE.md` has it.
|
|
29
|
+
- Do not restate history — `docs/daily-log.md` and `git log` hold it.
|
|
30
|
+
|
|
31
|
+
## Rules live in CLAUDE.md — the ones people forget
|
|
32
|
+
|
|
33
|
+
Queue edits only through accept/watch/retire or explicit approval · never
|
|
34
|
+
generate comment text · 28-day title freeze + ≤8 metadata changes/week ·
|
|
35
|
+
decisions ride the 90-day window · proposals never self-promote.
|
|
36
|
+
|
|
37
|
+
## Then hand off to the right skill
|
|
38
|
+
|
|
39
|
+
`n-seo-triage` for "what should I do today", `n-seo-ship` to implement one
|
|
40
|
+
card, `n-seo-review` for the weekly pass, `n-seo-setup` and `n-seo-add-site`
|
|
41
|
+
for configuration. `.claude/skills/README.md` says which skills are for
|
|
42
|
+
operating an instance and which are for developing the engine.
|
|
43
|
+
|
|
44
|
+
## If something is broken
|
|
45
|
+
|
|
46
|
+
- Dashboard down: `npm start` in-place, or
|
|
47
|
+
`node <engine>/bin/n-seo.mjs start --instance <dir>` in engine+instance
|
|
48
|
+
mode, or if it runs as a service,
|
|
49
|
+
`launchctl kickstart -k gui/$(id -u)/n-seo.dashboard`.
|
|
50
|
+
- Daily run failing: the Logs page (`/logs`) or `data/daily-ops.log`, then
|
|
51
|
+
`python3 ops/doctor.py`. 401/403 from Google means the service account
|
|
52
|
+
lost access or the key file moved — see `docs/SETUP-GOOGLE.md`.
|
|
53
|
+
- Nothing on the dashboard: no run has happened yet — `python3 ops/daily.py`
|
|
54
|
+
(or `python3 ops/demo_data.py` to see it populated with synthetic data).
|
package/README.md
CHANGED
|
@@ -24,8 +24,10 @@ to **a model you choose**, and everything that changes a site is handed to you.
|
|
|
24
24
|
ships with n-seo. Leave it off and you still get the full data-derived queue.
|
|
25
25
|
- **Your coding agent, on the queue.** A read-only MCP server exposes the same
|
|
26
26
|
data the dashboard reads, so Claude Code — or any MCP client — can answer
|
|
27
|
-
"what should I do first this week?" and then go implement it.
|
|
28
|
-
|
|
27
|
+
"what should I do first this week?" and then go implement it. Seven skills
|
|
28
|
+
are installed into your instance, so setup, triage and shipping are things
|
|
29
|
+
you ask for in plain language, with the operating rules enforced rather than
|
|
30
|
+
merely documented.
|
|
29
31
|
- **It proposes; it never acts.** No module edits a site, sends an email or
|
|
30
32
|
posts a comment. Machine proposals wait in a holding area until you accept
|
|
31
33
|
them. Your data stays on your hardware: nothing leaves the host except the
|
|
@@ -35,11 +37,61 @@ to **a model you choose**, and everything that changes a site is handed to you.
|
|
|
35
37
|
|
|
36
38
|
## Quickstart (5 minutes, no Google setup)
|
|
37
39
|
|
|
40
|
+
> **n-seo is its own project — do not install it inside a website's repo.**
|
|
41
|
+
> It reads your sites the way Google sees them, through the Search Console and
|
|
42
|
+
> GA4 APIs, so it never needs a copy of their code. `n-seo init` makes one
|
|
43
|
+
> directory of its own and everything it writes stays there.
|
|
44
|
+
|
|
45
|
+
**✗ Inside a site's repo** — what people do by accident:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
~/code/my-website/
|
|
49
|
+
├── .git/
|
|
50
|
+
├── package.json
|
|
51
|
+
├── src/
|
|
52
|
+
└── my-sites/ ← init ran here
|
|
53
|
+
├── n-seo.config.json committed to your website
|
|
54
|
+
├── config/backlog.json committed
|
|
55
|
+
├── .env committed
|
|
56
|
+
└── data/ rewritten every morning,
|
|
57
|
+
committed, then deployed
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**✓ Beside it** — one instance, watching every site you own:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
~/
|
|
64
|
+
├── code/
|
|
65
|
+
│ ├── my-website/ untouched
|
|
66
|
+
│ └── docs-site/ untouched
|
|
67
|
+
└── my-sites/ ← n-seo init my-sites
|
|
68
|
+
├── n-seo.config.json lists both sites
|
|
69
|
+
├── config/backlog.json your action queue
|
|
70
|
+
├── content/ drafts, campaigns
|
|
71
|
+
├── .claude/skills/ ask, don't read docs
|
|
72
|
+
└── data/ gitignored, regenerable
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`n-seo init` refuses the first layout: it stops when the target holds a
|
|
76
|
+
`package.json`, or when the directory it would create falls inside a git
|
|
77
|
+
repository that is not itself an instance, and prints the command you wanted
|
|
78
|
+
instead. `--force` overrides it.
|
|
79
|
+
|
|
80
|
+
**What it touches.** Your site repos: never — when you ship a fix it is you,
|
|
81
|
+
in your repo, on a branch. Google's APIs: read-only, through a service account
|
|
82
|
+
you create. Its own directory: everything it writes. Delete that directory and
|
|
83
|
+
nothing else on your machine changes.
|
|
84
|
+
|
|
38
85
|
```sh
|
|
39
|
-
npm install -g n-seo
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
n-seo
|
|
86
|
+
npm install -g n-seo # the engine, once — a CLI, not a dependency
|
|
87
|
+
|
|
88
|
+
cd ~ # stand somewhere that is NOT a site repo
|
|
89
|
+
n-seo init my-sites # creates ./my-sites: config, queue, content,
|
|
90
|
+
cd my-sites # .mcp.json and seven skills
|
|
91
|
+
|
|
92
|
+
n-seo demo # synthetic data, so every page has something
|
|
93
|
+
n-seo start # the dashboard, reading this directory
|
|
94
|
+
# → http://localhost:4600
|
|
43
95
|
```
|
|
44
96
|
|
|
45
97
|
Open http://localhost:4600. Every page is populated from the demo data, so you
|
|
@@ -48,6 +100,29 @@ removes it.
|
|
|
48
100
|
|
|
49
101
|
Prefer not to install globally? `npx n-seo init my-sites` works the same way.
|
|
50
102
|
|
|
103
|
+
### Then talk to it
|
|
104
|
+
|
|
105
|
+
`n-seo init` installs seven skills into the new directory, so the rest of the
|
|
106
|
+
setup is a conversation rather than a docs crawl. Open it in Claude Code — or
|
|
107
|
+
any agent that reads `.claude/skills/` — and say what you want:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
cd my-sites && claude
|
|
111
|
+
|
|
112
|
+
"set this up for my sites" → /n-seo-setup: config, the Google service
|
|
113
|
+
account, both console grants, first run
|
|
114
|
+
"add learn-pretext.com" → /n-seo-add-site
|
|
115
|
+
"what should I work on today?" → /n-seo-triage: the queue, with evidence
|
|
116
|
+
"do the first one" → /n-seo-ship: a branch and a PR in the
|
|
117
|
+
site's own repo, freeze rules enforced
|
|
118
|
+
"how did last month go?" → /n-seo-review
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A read-only MCP server is registered in the generated `.mcp.json`, so the agent
|
|
122
|
+
reads the same queue and metrics the dashboard shows. It can propose and
|
|
123
|
+
implement; it cannot publish, post, or change a site behind your back. Details
|
|
124
|
+
in [Agentic by design](#agentic-by-design).
|
|
125
|
+
|
|
51
126
|
<details>
|
|
52
127
|
<summary>Or run it from a clone, if you want to change the engine itself</summary>
|
|
53
128
|
|
|
@@ -59,10 +134,11 @@ npm run demo
|
|
|
59
134
|
npm start
|
|
60
135
|
```
|
|
61
136
|
|
|
62
|
-
In this mode the config, queue and data live inside the checkout
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
137
|
+
In this mode the config, queue and data live inside the checkout — still a
|
|
138
|
+
standalone project of its own, just one you can edit. That is the right shape
|
|
139
|
+
for hacking on n-seo; for running it, the instance layout above keeps your
|
|
140
|
+
files separate from the engine so upgrades are a reinstall rather than a
|
|
141
|
+
merge. See [docs/INSTANCE.md](docs/INSTANCE.md).
|
|
66
142
|
</details>
|
|
67
143
|
|
|
68
144
|
## Connect your real sites
|
|
@@ -149,8 +225,10 @@ Desktop, stdio and authenticated HTTP clients are covered in
|
|
|
149
225
|
working here has to follow.
|
|
150
226
|
|
|
151
227
|
Skills ship for the work itself, so the rules are enforced rather than
|
|
152
|
-
merely documented
|
|
153
|
-
|
|
228
|
+
merely documented. `n-seo init` copies all seven into the instance's
|
|
229
|
+
`.claude/skills/` with this install's real paths substituted in, alongside a
|
|
230
|
+
`CLAUDE.md` carrying the operating rules — so an agent opened in that
|
|
231
|
+
directory is oriented before you type anything:
|
|
154
232
|
|
|
155
233
|
| Skill | Use it when |
|
|
156
234
|
|---|---|
|
|
@@ -163,9 +241,9 @@ current in a fresh session:
|
|
|
163
241
|
| `orient` | First thing in a fresh session — get current in a few reads |
|
|
164
242
|
|
|
165
243
|
`n-seo-ship` stops rather than crossing the 28-day title freeze or the weekly
|
|
166
|
-
metadata budget.
|
|
167
|
-
|
|
168
|
-
|
|
244
|
+
metadata budget. The engine checkout carries a second set of skills (`ndx-*`)
|
|
245
|
+
for developing n-seo itself with [n-dx](https://n-dx.dev); those are not
|
|
246
|
+
copied into an instance. `.claude/skills/README.md` explains the split.
|
|
169
247
|
|
|
170
248
|
Read-only is the point: an agent can reason over your search data all day and
|
|
171
249
|
still cannot bypass the freeze, the batching, or you.
|
package/bin/n-seo.mjs
CHANGED
|
@@ -28,7 +28,9 @@ const USAGE = `n-seo ${PKG.version} — an SEO on your machine
|
|
|
28
28
|
|
|
29
29
|
usage: n-seo <command> [--instance <dir>] [args...]
|
|
30
30
|
|
|
31
|
-
init [dir] scaffold
|
|
31
|
+
init [dir] scaffold a standalone instance directory (default: cwd); never
|
|
32
|
+
overwrites, and refuses to scaffold inside a website's repo
|
|
33
|
+
(--force overrides)
|
|
32
34
|
start run the dashboard for the instance
|
|
33
35
|
dev dashboard with reload on engine code changes
|
|
34
36
|
daily the morning run (args pass through: --only, --skip, --list, …)
|
|
@@ -142,8 +144,87 @@ function missingDeps(cmd, what) {
|
|
|
142
144
|
|
|
143
145
|
/* ---------- init ---------- */
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
/** Files that mean "this directory is an application", not a place to keep
|
|
148
|
+
* search-ops files. n-seo is a standalone project: it writes a config, a
|
|
149
|
+
* queue, drafts and a data/ tree that the daily run rewrites every morning.
|
|
150
|
+
* Dropped into a website's repo those files get committed, deployed, and
|
|
151
|
+
* eventually served — and `n-seo upgrade` starts fighting the site's own
|
|
152
|
+
* package.json. The instance sits beside the sites it watches, never inside
|
|
153
|
+
* one. */
|
|
154
|
+
const APP_MARKERS = [
|
|
155
|
+
"package.json", "index.html", "next.config.js", "next.config.mjs", "next.config.ts",
|
|
156
|
+
"vite.config.js", "vite.config.ts", "astro.config.mjs", "nuxt.config.ts",
|
|
157
|
+
"svelte.config.js", "gatsby-config.js", "remix.config.js", "angular.json",
|
|
158
|
+
"Gemfile", "go.mod", "Cargo.toml", "pyproject.toml", "composer.json",
|
|
159
|
+
"hugo.toml", "config.toml", "_config.yml", "wp-config.php", "Dockerfile",
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/** The nearest ancestor holding a .git, or null. */
|
|
163
|
+
function gitRootOf(dir) {
|
|
164
|
+
let cur = path.resolve(dir);
|
|
165
|
+
for (;;) {
|
|
166
|
+
if (fs.existsSync(path.join(cur, ".git"))) return cur;
|
|
167
|
+
const up = path.dirname(cur);
|
|
168
|
+
if (up === cur) return null;
|
|
169
|
+
cur = up;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Why this directory is the wrong place for an instance, or null if it is
|
|
174
|
+
* fine.
|
|
175
|
+
*
|
|
176
|
+
* `forChild` means the instance will be a NEW directory created inside
|
|
177
|
+
* `dir`, which changes one answer: a repository root is a fine place to put
|
|
178
|
+
* an instance (it becomes its own repo) but a terrible parent for one (the
|
|
179
|
+
* new directory lands inside that repo's working tree).
|
|
180
|
+
*
|
|
181
|
+
* Re-running init on an existing instance is always allowed. */
|
|
182
|
+
function wrongPlaceFor(dir, forChild = false) {
|
|
183
|
+
if (fs.existsSync(path.join(dir, "n-seo.config.json"))) return null;
|
|
184
|
+
// In-place mode: the engine checkout is the instance. It has a package.json
|
|
185
|
+
// and a .git of its own, both of which would otherwise trip every check.
|
|
186
|
+
if (!forChild && path.resolve(dir) === ROOT) return null;
|
|
187
|
+
const marker = APP_MARKERS.find((f) => fs.existsSync(path.join(dir, f)));
|
|
188
|
+
if (marker) {
|
|
189
|
+
return forChild
|
|
190
|
+
? `${dir} contains ${marker} — an instance created here would sit inside an application`
|
|
191
|
+
: `${dir} contains ${marker} — that is an application, not an n-seo instance`;
|
|
192
|
+
}
|
|
193
|
+
const root = gitRootOf(dir);
|
|
194
|
+
const insideSomeoneElsesRepo = root && (forChild || root !== path.resolve(dir));
|
|
195
|
+
if (insideSomeoneElsesRepo && !fs.existsSync(path.join(root, "n-seo.config.json"))) {
|
|
196
|
+
return `${dir} is inside the git repository at ${root} — the instance would be committed to it`;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function init(dirArg, force = false) {
|
|
146
202
|
const dir = path.resolve(dirArg ?? process.cwd());
|
|
203
|
+
// A directory that does not exist yet inherits its parent's problem: a new
|
|
204
|
+
// folder created inside a site repo is still inside that site repo.
|
|
205
|
+
const wrong = fs.existsSync(dir) ? wrongPlaceFor(dir) : wrongPlaceFor(path.dirname(dir), true);
|
|
206
|
+
if (wrong && !force) {
|
|
207
|
+
console.error(`n-seo init: refusing to scaffold here.
|
|
208
|
+
|
|
209
|
+
${wrong}
|
|
210
|
+
|
|
211
|
+
n-seo is a standalone project, not something you add to a website. It keeps a
|
|
212
|
+
config, an action queue, content drafts and a data/ tree that the daily run
|
|
213
|
+
rewrites every morning; inside a site repo those get committed and deployed.
|
|
214
|
+
The instance watches your sites over the Search Console API — it never needs
|
|
215
|
+
to live in their code.
|
|
216
|
+
|
|
217
|
+
Do this instead:
|
|
218
|
+
|
|
219
|
+
cd ~ # anywhere outside your site repos
|
|
220
|
+
n-seo init my-sites
|
|
221
|
+
cd my-sites
|
|
222
|
+
|
|
223
|
+
Then list the sites you own in my-sites/n-seo.config.json.
|
|
224
|
+
|
|
225
|
+
If you really mean it here, pass --force.`);
|
|
226
|
+
return 2;
|
|
227
|
+
}
|
|
147
228
|
fs.mkdirSync(dir, { recursive: true });
|
|
148
229
|
const name = path.basename(dir);
|
|
149
230
|
const put = (rel, content) => {
|
|
@@ -170,6 +251,59 @@ function init(dirArg) {
|
|
|
170
251
|
const src = path.join(ROOT, rel);
|
|
171
252
|
put(rel, fs.existsSync(src) ? fs.readFileSync(src, "utf8") : `# ${rel}\n`);
|
|
172
253
|
}
|
|
254
|
+
// The skills are the product's front door: with them in place the instance
|
|
255
|
+
// is a directory an agent can be pointed at, and setup / triage / shipping
|
|
256
|
+
// become a conversation instead of a docs-reading exercise. The engine's
|
|
257
|
+
// own contributor skills (ndx-*) stay behind — they are for working on
|
|
258
|
+
// n-seo, not with it. Placeholders become this install's real paths so the
|
|
259
|
+
// commands in them are copy-pasteable.
|
|
260
|
+
const INSTANCE_SKILLS = ["orient", "n-seo-setup", "n-seo-add-site", "n-seo-triage", "n-seo-ship", "n-seo-review", "n-seo-deploy"];
|
|
261
|
+
let skillsCopied = 0;
|
|
262
|
+
for (const skill of INSTANCE_SKILLS) {
|
|
263
|
+
const src = path.join(ROOT, ".claude", "skills", skill, "SKILL.md");
|
|
264
|
+
if (!fs.existsSync(src)) continue;
|
|
265
|
+
const body = fs
|
|
266
|
+
.readFileSync(src, "utf8")
|
|
267
|
+
.replaceAll("<engine checkout>/bin/n-seo.mjs", path.join(ROOT, "bin", "n-seo.mjs"))
|
|
268
|
+
.replaceAll("<instance dir>", dir);
|
|
269
|
+
put(path.join(".claude", "skills", skill, "SKILL.md"), body);
|
|
270
|
+
skillsCopied++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
put("CLAUDE.md", `# ${name} — an n-seo instance
|
|
274
|
+
|
|
275
|
+
This directory is an n-seo instance: config, the curated action queue, content
|
|
276
|
+
drafts, and the data the daily run writes. It is **not** a website. The sites
|
|
277
|
+
it watches live in their own repositories and are read over the Search Console
|
|
278
|
+
and GA4 APIs.
|
|
279
|
+
|
|
280
|
+
Engine (the code): ${ROOT}
|
|
281
|
+
Docs: ${path.join(ROOT, "docs")}
|
|
282
|
+
|
|
283
|
+
## Working here
|
|
284
|
+
|
|
285
|
+
Skills in \`.claude/skills/\` cover the routine work — start with
|
|
286
|
+
\`/orient\` in a fresh session, then \`/n-seo-triage\` for what to do today,
|
|
287
|
+
\`/n-seo-ship\` to implement one card, \`/n-seo-review\` for the weekly pass.
|
|
288
|
+
|
|
289
|
+
## Rules that are not negotiable
|
|
290
|
+
|
|
291
|
+
- **28-day metadata freeze.** After a page's title or description changes,
|
|
292
|
+
leave that page's metadata alone for 28 days. Title churn reads as
|
|
293
|
+
manipulation and resets Google's evaluation.
|
|
294
|
+
- **At most ~8 title/description changes a week** across all sites.
|
|
295
|
+
- **Impact numbers order the queue. They are not forecasts.** Never report
|
|
296
|
+
one as an expected result.
|
|
297
|
+
- **Decisions ride the trailing 90 days.** The 16-month data is for totals
|
|
298
|
+
and history only.
|
|
299
|
+
- **Shipped work becomes \`watching\`, never deleted** — that is how the loop
|
|
300
|
+
closes.
|
|
301
|
+
- **Machine proposals never self-promote.** They wait in the holding area on
|
|
302
|
+
/actions until a human accepts them.
|
|
303
|
+
- **Site changes ship as branches and pull requests** in the site's own repo,
|
|
304
|
+
never committed straight to its main branch.
|
|
305
|
+
`);
|
|
306
|
+
|
|
173
307
|
const envExample = path.join(ROOT, ".env.example");
|
|
174
308
|
put(".env", fs.existsSync(envExample) ? fs.readFileSync(envExample, "utf8") : "");
|
|
175
309
|
put(".gitignore", "data/\nsite/\n.env\n__pycache__/\nnode_modules/\n");
|
|
@@ -203,15 +337,38 @@ n-seo upgrade # update the engine; your files here are untouched
|
|
|
203
337
|
|
|
204
338
|
Edit \`n-seo.config.json\` for sites, Google access, modules and hooks.
|
|
205
339
|
Docs: ${path.join(ROOT, "docs")}
|
|
340
|
+
|
|
341
|
+
## Or just ask
|
|
342
|
+
|
|
343
|
+
n-seo is built to be driven by an AI agent. Open this directory in Claude Code
|
|
344
|
+
(or any agent that reads \`.claude/skills/\`) and talk to it:
|
|
345
|
+
|
|
346
|
+
"set this up for my sites" → /n-seo-setup
|
|
347
|
+
"what should I work on today?" → /n-seo-triage
|
|
348
|
+
"do the first one" → /n-seo-ship
|
|
349
|
+
"how did last month go?" → /n-seo-review
|
|
350
|
+
|
|
351
|
+
A read-only MCP server is wired up in \`.mcp.json\`, so the agent reads the
|
|
352
|
+
same queue and metrics the dashboard shows. It can propose and implement;
|
|
353
|
+
it cannot publish, post, or change your sites behind your back.
|
|
206
354
|
`);
|
|
207
355
|
console.log(`
|
|
208
356
|
instance ready at ${dir}
|
|
357
|
+
a standalone project — your websites stay in their own repos, untouched
|
|
209
358
|
|
|
210
359
|
next:
|
|
211
360
|
cd ${dir}
|
|
212
|
-
|
|
213
|
-
n-seo
|
|
214
|
-
n-seo
|
|
361
|
+
n-seo demo && n-seo start see the dashboard working on synthetic data
|
|
362
|
+
edit n-seo.config.json your sites + google.serviceAccountKey
|
|
363
|
+
n-seo doctor checks the setup and names what is missing
|
|
364
|
+
|
|
365
|
+
or hand it to an agent — ${skillsCopied} skills are installed in .claude/skills/:
|
|
366
|
+
cd ${dir} && claude
|
|
367
|
+
"set this up for my sites" → /n-seo-setup walks the whole thing, including
|
|
368
|
+
the Google service account and both grants
|
|
369
|
+
later: "what should I work on today?" → /n-seo-triage
|
|
370
|
+
|
|
371
|
+
setup guide: ${path.join(ROOT, "docs", "SETUP-GOOGLE.md")}
|
|
215
372
|
`);
|
|
216
373
|
return 0;
|
|
217
374
|
}
|
|
@@ -264,7 +421,7 @@ let code = 0;
|
|
|
264
421
|
|
|
265
422
|
switch (cmd) {
|
|
266
423
|
case "init":
|
|
267
|
-
code = init(rest
|
|
424
|
+
code = init(rest.find((a) => !a.startsWith("-")) ?? flag ?? process.cwd(), rest.includes("--force"));
|
|
268
425
|
break;
|
|
269
426
|
case "start":
|
|
270
427
|
case "dev":
|
package/docs/ADDING-A-SITE.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
One entry in `n-seo.config.json`. There are no per-script site lists:
|
|
4
4
|
every pull, probe, audit, page and export reads the same `sites` array.
|
|
5
5
|
|
|
6
|
+
> Or say *"add example.com"* in the instance directory: the `n-seo-add-site`
|
|
7
|
+
> skill picks the right property form, finds the numeric GA4 id, proposes a
|
|
8
|
+
> brand regex, and reminds you about both access grants.
|
|
9
|
+
|
|
6
10
|
```json
|
|
7
11
|
{
|
|
8
12
|
"host": "docs.example.com",
|
package/docs/DEPLOY.md
CHANGED
|
@@ -7,6 +7,10 @@ it sleeps, it travels, and the run silently skips a day.
|
|
|
7
7
|
|
|
8
8
|
This document is about moving it to a box that is always on.
|
|
9
9
|
|
|
10
|
+
> Or say *"get this off my laptop"* in the instance directory — the
|
|
11
|
+
> `n-seo-deploy` skill runs this procedure, and refuses to expose the
|
|
12
|
+
> dashboard port to the internet whatever you ask it for.
|
|
13
|
+
|
|
10
14
|
## The shape
|
|
11
15
|
|
|
12
16
|
One host runs two long-lived things — the dashboard and a scheduler that
|
package/docs/FAQ.md
CHANGED
|
@@ -5,6 +5,25 @@ Nothing. The Search Console and GA4 APIs are free within quotas that ordinary
|
|
|
5
5
|
use never approaches. The optional LLM module runs whatever local command you
|
|
6
6
|
give it; if that is a paid CLI, that is your cost, not the tool's.
|
|
7
7
|
|
|
8
|
+
**Can I install it inside my website's repository?**
|
|
9
|
+
No — and `n-seo init` refuses to, so you cannot do it by accident. n-seo is a
|
|
10
|
+
standalone project. It writes a config, an action queue, content drafts and a
|
|
11
|
+
`data/` tree that the daily run rewrites every morning; inside a site repo all
|
|
12
|
+
of that gets committed, and usually deployed. It reads your sites through the
|
|
13
|
+
Search Console and GA4 APIs, so it has no reason to sit in their code. Put the
|
|
14
|
+
instance anywhere else — `~/my-sites` is fine — and list the sites you own in
|
|
15
|
+
its config. One instance can watch as many sites as you like. `--force`
|
|
16
|
+
overrides the check if you have a reason.
|
|
17
|
+
|
|
18
|
+
**Do I have to read all these docs?**
|
|
19
|
+
No. `n-seo init` installs seven skills into your instance, so setup and daily
|
|
20
|
+
use are a conversation: open the directory in Claude Code (or any agent that
|
|
21
|
+
reads `.claude/skills/`) and say "set this up for my sites", "what should I
|
|
22
|
+
work on today?", "do the first one". The agent reads the queue over a
|
|
23
|
+
read-only MCP server, follows the operating rules in the generated
|
|
24
|
+
`CLAUDE.md`, and cannot publish or change a site on its own. The docs are the
|
|
25
|
+
reference behind the skills, not a prerequisite.
|
|
26
|
+
|
|
8
27
|
**Does it change my site?**
|
|
9
28
|
No. It reads your data, probes your pages, and produces a queue of actions
|
|
10
29
|
with evidence and specs. You make the change in your own repo and ship it.
|
|
@@ -89,9 +108,19 @@ shown flat rather than omitted so you can tell "connected and quiet" from
|
|
|
89
108
|
"missing from the pipeline."
|
|
90
109
|
|
|
91
110
|
**The indexing page lists a "Soft 404" for a page that is fine.**
|
|
92
|
-
Check the "last crawled" column. A verdict is only as current as the
|
|
93
|
-
behind it
|
|
94
|
-
|
|
111
|
+
Check the "last crawled" column first. A verdict is only as current as the
|
|
112
|
+
crawl behind it, and the page flags verdicts older than 90 days as stale — a
|
|
113
|
+
page rewritten since then is being judged on content Google has not seen. For
|
|
114
|
+
a stale verdict, request indexing once and let the recrawl settle it.
|
|
115
|
+
|
|
116
|
+
For a *fresh* soft 404, requesting indexing is the wrong move: Google already
|
|
117
|
+
fetched the page, got a 200, and decided the content was an error or empty.
|
|
118
|
+
Asking again re-runs the same judgement and spends the daily quota. Fetch the
|
|
119
|
+
URL yourself and pick the real cause — the page renders its content in
|
|
120
|
+
JavaScript and the crawler saw an empty shell, or it is genuinely thin, or it
|
|
121
|
+
is an error page returning 200 and should return 404 or 410 and leave the
|
|
122
|
+
sitemap. Fix that, then request indexing. n-seo's indexing page names the
|
|
123
|
+
check to run for each verdict.
|
|
95
124
|
|
|
96
125
|
**A daily step failed. What now?**
|
|
97
126
|
Open `/logs` (or `data/daily-ops.log`), then run `n-seo doctor`. A 401
|
package/docs/INSTANCE.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Running n-seo as an engine + instance
|
|
2
2
|
|
|
3
|
+
> **An instance is a standalone project.** It is never a subdirectory of a
|
|
4
|
+
> website you are optimising. n-seo reads your sites through the Search
|
|
5
|
+
> Console and GA4 APIs; it has no reason to live in their code, and living
|
|
6
|
+
> there means committing and deploying its config, its queue and a `data/`
|
|
7
|
+
> tree that is rewritten every morning.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
~/
|
|
11
|
+
├── code/
|
|
12
|
+
│ ├── my-website/ a site you own — untouched
|
|
13
|
+
│ └── docs-site/ another one — untouched
|
|
14
|
+
└── my-sites/ the instance
|
|
15
|
+
├── n-seo.config.json both sites listed here
|
|
16
|
+
├── config/backlog.json the curated queue
|
|
17
|
+
├── content/ drafts, campaigns
|
|
18
|
+
├── .claude/skills/ the operating skills
|
|
19
|
+
├── CLAUDE.md the operating rules
|
|
20
|
+
└── data/ the daily run's output, gitignored
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
One instance watches as many sites as you own, and `n-seo init` refuses to
|
|
24
|
+
scaffold into an application directory or a git repository it does not own
|
|
25
|
+
(`--force` overrides).
|
|
26
|
+
|
|
3
27
|
n-seo can run two ways. In the simplest, you clone the repo, put your config
|
|
4
28
|
in the checkout and run it there. In the second, the checkout is an **engine**
|
|
5
29
|
you never edit, and everything that is yours — config, queue, content, data,
|
|
@@ -93,6 +117,36 @@ and `content/campaigns/README.md`, `.env` (from `.env.example`), a
|
|
|
93
117
|
`.gitignore` that excludes `data/`, `site/` and `.env`, a `.mcp.json` that
|
|
94
118
|
spawns the engine's MCP server with `N_SEO_INSTANCE` set, and a short README.
|
|
95
119
|
|
|
120
|
+
It also makes the instance agent-ready, which is how most people should drive
|
|
121
|
+
it:
|
|
122
|
+
|
|
123
|
+
- `.claude/skills/` — the seven operating skills (`orient`, `n-seo-setup`,
|
|
124
|
+
`n-seo-add-site`, `n-seo-triage`, `n-seo-ship`, `n-seo-review`,
|
|
125
|
+
`n-seo-deploy`), copied from the engine with this install's real engine and
|
|
126
|
+
instance paths substituted in, so every command in them is copy-pasteable.
|
|
127
|
+
The engine's contributor skills (`ndx-*`) are not copied — they are for
|
|
128
|
+
developing n-seo, not operating it.
|
|
129
|
+
- `CLAUDE.md` — the operating rules an agent working in this directory has to
|
|
130
|
+
follow: the 28-day freeze, the weekly metadata budget, impact-is-not-a-
|
|
131
|
+
forecast, decisions on the 90-day window, shipped-becomes-watching,
|
|
132
|
+
proposals never self-promote, site changes ship as pull requests.
|
|
133
|
+
|
|
134
|
+
With those two files plus `.mcp.json`, opening the instance in Claude Code is
|
|
135
|
+
enough: it can read the queue and the metrics, and it already knows the rules.
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
cd ~/sites/search-ops && claude
|
|
139
|
+
"set this up for my sites" → /n-seo-setup
|
|
140
|
+
"what should I work on today?" → /n-seo-triage
|
|
141
|
+
"do the first one" → /n-seo-ship
|
|
142
|
+
"how did last month go?" → /n-seo-review
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Both files are yours once written. `n-seo init` never overwrites, so editing
|
|
146
|
+
`CLAUDE.md` to add your own rules survives every re-run and every engine
|
|
147
|
+
upgrade. To pick up improved skills from a newer engine, delete the ones you
|
|
148
|
+
have not customised and re-run `n-seo init`.
|
|
149
|
+
|
|
96
150
|
To make `n-seo` a command, link the engine once:
|
|
97
151
|
|
|
98
152
|
```sh
|