README down to what a user needs; the rest into docs/

The README had grown to 1,010 lines of user docs and design notes in one file,
and had gone stale: it still showed the old numbered menu, set up only three
providers, and listed preflight checks and a file layout that predate the last
three releases. It now carries install, first run, the full command
reference, the providers at a glance, troubleshooting and a docs index.

docs/:
- providers.md    presets, defaults, the model cache, context windows, each
                  provider (OpenRouter's cost guard and guardrail check are
                  written up for the first time), adding a provider
- live-sessions.md  what a switch does to running sessions, and repair
- design.md       why settings.json, why keys stay out of it (and the vault
                  per platform), the preflight checks as they are now
- bar-widget.md   the widget as it is now: providers from health.json, every
                  server provider's settings, the restart after an upgrade
- architecture.md the pieces, every file on disk and who writes it, the
                  contracts between them, where to change what
- development.md  running the tests, the conventions the code follows,
                  working on the widget, releasing
CONTRIBUTING.md points at it.

Also:
- The per-project session listing used awk, which the CLI avoids because it
  is missing from minimal images; it uses the script's own TSV helpers now,
  and tests/static.sh fails on any awk in the CLI.
- tests/static.sh checks every relative Markdown link and #anchor.
- A unit test pins the managed env keys between cm-json.py and
  claude-mode.ps1, which only a comment kept in step before.
- test_sessions covers the per-project listing, which nothing ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
smoido
2026-09-15 01:52:03 +03:00
co-authored by Claude Opus 5
parent 51e42823d1
commit b514e00745
12 changed files with 1210 additions and 945 deletions
+9 -1
View File
@@ -2,7 +2,11 @@
# Broken transcripts: the scan, dismissing and restoring, the age rule, repair.
. "$(dirname "$0")/../lib.sh"
proj="$CLAUDE_CONFIG_DIR/projects/-tmp-fixture"
# The fixture project is filed under the slug of a real directory, so the
# per-project listing (run from inside it) finds it the way Claude Code would.
work="$SB/work"
mkdir -p "$work"
proj="$CLAUDE_CONFIG_DIR/projects/$(printf '%s' "$work" | sed 's|[^a-zA-Z0-9]|-|g')"
mkdir -p "$proj"
transcript() { # <session-id> [age-in-hours]
printf '%s\n' \
@@ -34,6 +38,10 @@ run_cm repair-session --all
expect_out '2 hidden: 1 ignored, 1 older than 7 days' 'hidden sessions are always counted'
run_cm repair-session --ignored
expect_out 'still broken' 'the dismissed list reconciles against the disk'
OUT="$(cd "$work" && "$CM" repair-session 2>&1 | sed "s/${ESC}\[[0-9;]*m//g")"
expect_out 'cccc-ok' 'the per-project listing shows every transcript here'
expect_out '(hidden: ignored)' 'and marks a dismissed one'
expect_out '(hidden: older than 7 days)' 'and an age-hidden one'
run_cm repair-session --ignore zzzz
expect_rc 1 'an unknown session cannot be dismissed'
run_cm repair-session --unignore aaaa-new
+13
View File
@@ -89,6 +89,19 @@ class Providers(unittest.TestCase):
with self.assertRaises(SystemExit):
call(cm.cmd_provider_resolve, [word])
def test_managed_keys_match_the_windows_build(self):
# A key one build clears and the other does not survives a switch on
# that platform - "must stay in lockstep" was only a comment until this.
import re
with open(os.path.join(REPO, "claude-mode.ps1"), encoding="utf-8") as fh:
ps = fh.read()
# Up to the line that is only ")", not the first ")": the array's own
# comments contain parentheses.
block = re.search(r"\$script:BaseManagedEnvKeys\s*=\s*@\((.*?)^\)", ps, re.S | re.M)
self.assertIsNotNone(block, "BaseManagedEnvKeys not found in claude-mode.ps1")
body = "\n".join(l for l in block.group(1).splitlines() if not l.strip().startswith("#"))
self.assertEqual(sorted(re.findall(r"'([A-Z0-9_]+)'", body)), sorted(cm.BASE_MANAGED))
def test_static_catalogue(self):
self.assertEqual(call(cm.cmd_provider_static, ["zai"]).splitlines()[0].split("\t")[0], "glm-5.3")
+37
View File
@@ -32,6 +32,13 @@ bash_syntax() {
}
check 'shell scripts parse (bash -n)' bash_syntax
# awk is absent from minimal images (a stock Fedora WSL rootfs, for one), so the
# CLI does without it - and one slipped in once, in a listing no test ran.
no_awk() {
! grep -nw awk linux/claude-mode linux/lib/*.sh 2>/dev/null | grep -vE '^[^:]+:[0-9]+:\s*#'
}
check 'no awk in the POSIX CLI' no_awk
check 'python compiles' env PYTHONPYCACHEPREFIX="$tmp" python3 -m py_compile \
linux/cm-json.py tests/fake_server.py tests/python/test_cm_json.py
@@ -118,6 +125,36 @@ if errors:
sys.exit("\n".join(errors))
PY
# Relative links, and the #anchors in them, against the headings they point at -
# so moving a section between files cannot leave a dead link behind. Anchors are
# slugged the way Gitea and GitHub do: lowercase, punctuation dropped, spaces to
# dashes.
check 'Markdown links resolve' python3 - <<'PY'
import glob, os, re, sys
def slug(title):
title = re.sub(r"`", "", title.strip().lower())
return re.sub(r"[^\w\- ]", "", title).replace(" ", "-")
def anchors(path):
text = open(path, encoding="utf-8").read()
text = re.sub(r"```.*?```", "", text, flags=re.S)
return {slug(m.group(1)) for m in re.finditer(r"^#{1,6}\s+(.+?)\s*$", text, flags=re.M)}
files = ["README.md", "CONTRIBUTING.md", "CHANGELOG.md"] + glob.glob("docs/*.md")
errors = []
for f in files:
text = re.sub(r"```.*?```", "", open(f, encoding="utf-8").read(), flags=re.S)
for target in re.findall(r"\]\(([^)\s]+)\)", text):
if re.match(r"[a-z]+:", target):
continue # http:, https:, mailto:
path, _, anchor = target.partition("#")
dest = os.path.normpath(os.path.join(os.path.dirname(f), path)) if path else f
if not os.path.exists(dest):
errors.append("%s: %s - no such file" % (f, target))
elif anchor and dest.endswith(".md") and anchor not in anchors(dest):
errors.append("%s: %s - no heading for #%s" % (f, target, anchor))
if errors:
sys.exit("\n".join(errors))
PY
QMLLINT="$(command -v qmllint || true)"
[ -z "$QMLLINT" ] && [ -x /usr/lib/qt6/bin/qmllint ] && QMLLINT=/usr/lib/qt6/bin/qmllint
if [ -n "$QMLLINT" ]; then