n-seo 0.6.0 → 0.6.1

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/CHANGELOG.md CHANGED
@@ -8,6 +8,24 @@ uses [Semantic Versioning](https://semver.org/).
8
8
 
9
9
  Nothing yet.
10
10
 
11
+ ## [0.6.1] - 2026-09-15
12
+
13
+ ### Fixed
14
+ - **The daily log threw away the reason a model call failed.** The llm module
15
+ reported `stderr` only, but a CLI may put its failure on stdout — `claude`
16
+ exits 1 and writes "Failed to authenticate" there — so the log read
17
+ `llm: exit 1:` with nothing after the colon. Every call, cause discarded.
18
+ It now falls back to stdout. This is the bug that matters here: it turned a
19
+ one-line diagnosis into a long one, and produced a confident wrong answer
20
+ along the way.
21
+ - **`doctor` reported a model command as healthy when it could not run.** The
22
+ check was `shutil.which`: the file exists, therefore fine. A revoked key, a
23
+ lapsed subscription or an unreadable credential store all leave the binary
24
+ exactly where it was. It now sends a trivial prompt and reports what came
25
+ back. Note the limitation — it reports what happens *in the environment you
26
+ run it in*. A stripped environment that cannot reach the system credential
27
+ store will fail this check while the scheduled job, which can, is fine.
28
+
11
29
  ## [0.6.0] - 2026-09-15
12
30
 
13
31
  ### Added
@@ -486,7 +504,8 @@ First public release.
486
504
  `n-seo upgrade` gate — without changing the reported test counts, and an
487
505
  in-place install ran those assertions against the owner's live data.
488
506
 
489
- [Unreleased]: https://github.com/en-dash-consulting/n-seo/compare/v0.6.0...HEAD
507
+ [Unreleased]: https://github.com/en-dash-consulting/n-seo/compare/v0.6.1...HEAD
508
+ [0.6.1]: https://github.com/en-dash-consulting/n-seo/compare/v0.6.0...v0.6.1
490
509
  [0.6.0]: https://github.com/en-dash-consulting/n-seo/compare/v0.5.0...v0.6.0
491
510
  [0.5.0]: https://github.com/en-dash-consulting/n-seo/compare/v0.4.1...v0.5.0
492
511
  [0.4.1]: https://github.com/en-dash-consulting/n-seo/compare/v0.4.0...v0.4.1
Binary file
package/ops/doctor.py CHANGED
@@ -79,6 +79,36 @@ def check_profile(cfg):
79
79
  report("OK", f"from {d}")
80
80
 
81
81
 
82
+ def probe_llm(cmd):
83
+ """Actually call the model and see whether it answers.
84
+
85
+ `shutil.which` only proves a file exists. An expired OAuth session, a
86
+ revoked key or a missing subscription all leave the binary exactly where
87
+ it was, so the old check reported OK while every call in the daily run
88
+ failed — eleven of them, silently, because the llm module degrades rather
89
+ than failing the run. A green tick for something that cannot work is
90
+ worse than no check at all.
91
+
92
+ Returns (ok, detail). Never raises.
93
+ """
94
+ try:
95
+ p = subprocess.run(cmd, input="Reply with the single word: ok",
96
+ capture_output=True, text=True, timeout=90, encoding="utf-8")
97
+ except subprocess.TimeoutExpired:
98
+ return False, "timed out after 90s"
99
+ except OSError as exc:
100
+ return False, str(exc)[:120]
101
+ if p.returncode != 0:
102
+ detail = (p.stderr or p.stdout or "").strip().splitlines()
103
+ return False, (detail[0][:160] if detail else f"exit {p.returncode}, no output")
104
+ out = (p.stdout or "").strip()
105
+ if not out:
106
+ # Some CLIs report an auth failure on stdout and still exit 0.
107
+ err = (p.stderr or "").strip().splitlines()
108
+ return False, (err[0][:160] if err else "exited 0 but said nothing")
109
+ return True, out.splitlines()[0][:60]
110
+
111
+
82
112
  def check_config():
83
113
  print("config")
84
114
  if seo_config.using_example():
@@ -301,8 +331,15 @@ def check_modules(cfg):
301
331
  elif key == "command" and not http and not llm.get("http"):
302
332
  report("FAIL", "llm has neither http nor command configured")
303
333
  continue
304
- report("OK" if shutil.which(cmd[0]) else "FAIL", f"llm.{key}: {cmd[0]}",
305
- "" if shutil.which(cmd[0]) else "not on PATH")
334
+ if not shutil.which(cmd[0]):
335
+ report("FAIL", f"llm.{key}: {cmd[0]}", "not on PATH")
336
+ continue
337
+ if OFFLINE:
338
+ report("OK", f"llm.{key}: {cmd[0]}", "on PATH — not called (offline)")
339
+ continue
340
+ ok, detail = probe_llm(cmd)
341
+ report("OK" if ok else "FAIL", f"llm.{key}: {raw[:48]}",
342
+ detail if not ok else f"answered: {detail}")
306
343
  hn = cfg["modules"].get("hackerNews", {})
307
344
  if hn.get("enabled"):
308
345
  user = (hn.get("user") or "").strip()
package/ops/llm.py CHANGED
@@ -155,7 +155,12 @@ def infer(prompt: str, fast: bool = False) -> str | None:
155
155
  print(f" llm: {exc}", file=sys.stderr)
156
156
  return None
157
157
  if p.returncode != 0:
158
- print(f" llm: exit {p.returncode}: {p.stderr.strip()[:200]}", file=sys.stderr)
158
+ # Not every CLI reports its failure on stderr. `claude` exits 1 and
159
+ # writes "Failed to authenticate: OAuth session expired" to stdout,
160
+ # so logging stderr alone produced "llm: exit 1:" with nothing after
161
+ # it — eleven times in one run, with the reason thrown away each time.
162
+ detail = (p.stderr.strip() or p.stdout.strip() or "no output")
163
+ print(f" llm: exit {p.returncode}: {detail[:200]}", file=sys.stderr)
159
164
  return None
160
165
  out = p.stdout.strip()
161
166
  return out or None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n-seo",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Agentic, local-first SEO / AEO / GEO control plane: pulls Search Console + GA4, probes your sites, and ranks the next moves. Your LLM writes the proposals; your coding agent works the queue over MCP.",
5
5
  "license": "MIT",
6
6
  "author": "En Dash Consulting (https://endash.us)",