Files
claude-mode/linux/cm-json.py
T
smoidoandClaude Opus 5 b5824c6611 Providers as data; add Ollama and Custom endpoints
Every gateway provider is now an entry in providers.json - endpoint and auth
template, how its model list is read, how it is probed before a switch, what
setup asks, which doctor checks apply, and its title, colour and logo -
installed next to the presets and read by all three consumers: the bash CLI
(through cm-json.py), the Windows script, and the bar widget (through
health.json). anthropic stays built in; it is the native login, not a gateway.

Behaviour that differs in kind stays in code, chosen by name from the entry:
catalogue parsers (openrouter, lmstudio, ollama, openai, static), probe rules
(always, lenient, local), and named doctor checks. A provider that reuses them
is an entry and a default preset, with no code. The widget draws providers
from health.json, so a new one needs no QML change and no shell restart.

The existing three are unchanged in behaviour: their blank presets come out
byte-identical from the file, and setup, doctor, models and the picker run the
same checks through the generic paths.

Ollama: local server on :11434, placeholder token, one model for every tier,
models from /api/tags. doctor reads the context each loaded model actually runs
with (/api/ps) and its maximum (/api/show), because Ollama defaults to 4096
tokens unless OLLAMA_CONTEXT_LENGTH is set and silently truncates past it. A
bare model name matches its :latest tag.

Custom: any Anthropic-compatible endpoint. Ships with no address and is refused
until it has one; key optional; models from /v1/models when the endpoint has a
list, and a lenient probe so a proxy without one is not blocked.

Also: preflight (and Set-ClaudeMode on Windows) refuses a preset with no
server address; the server form, setup and set-auth use each provider's own
default URL, key name and placeholder token instead of LM Studio's; the
Windows build gains the no-models and no-address guards it never had.
Tested on Linux against fake Ollama/Custom servers, and on Windows 5.1 in a
USERPROFILE sandbox on winbox.

1.12.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 01:20:33 +03:00

1525 lines
58 KiB
Python

#!/usr/bin/env python3
"""
JSON engine for claude-mode (POSIX port).
Bash cannot safely read or rewrite JSON, and jq is not installed often enough to
depend on. Everything that touches structured data goes through here, so the
shell script only ever handles flat lines of text.
Subcommands:
apply <settings> <state> <mode> [preset] [helper] rewrite the managed keys
summary <preset> human lines for the UI
models <preset> "tier<TAB>model" lines
set-tier <preset> <tier> <model> edit one tier in place
scaffold <provider> print a blank preset
get <file> <dotted.path> print one value
presets <dir> "name<TAB>provider<TAB>desc"
scan-sessions <projects> [max-age-days] [ignored] broken transcripts, as JSON
ignore-session <ignored> <projects> add|remove|clear|list [id]
cache-models <cache> <provider> <1|0> [baseUrl] store a TSV catalogue (stdin)
preset-rename <dir> <old> <new> <state> [defaults] rename, repointing state + default
auth-of <preset> "mode<TAB>keyRef"; fails if missing
set-default <defaults> <provider> [name] choose (or clear) a provider default
provider-tsv | provider-resolve <word> | provider-get <id> <path> | provider-static <id>
read providers.json
ollama-models | openai-models catalogue parsers (JSON on stdin)
ollama-ctx show | ps <model> context lengths from Ollama (stdin)
"""
import glob
import json
import os
import re
import shlex
import sys
# Matches both the qualified gateway id (anthropic/claude-opus-5) and the bare
# internal id Claude Code persists (claude-opus-5, claude-haiku-4-5-20251001).
ANTHROPIC_MODEL_RE = re.compile(r"(^|/)claude[-.]|^anthropic/")
def is_anthropic_model(model_id):
return bool(model_id) and bool(ANTHROPIC_MODEL_RE.search(str(model_id)))
# Must stay in lockstep with the Windows build's $script:BaseManagedEnvKeys.
BASE_MANAGED = [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
# Both pin a concrete model outside the tier mapping. A stale value in either
# survives a switch, and the small/fast slot is used by background
# summarisation - a known cause of "works normally, dies on compaction".
"ANTHROPIC_MODEL",
"ANTHROPIC_SMALL_FAST_MODEL",
"CLAUDE_CODE_SUBAGENT_MODEL",
"CLAUDE_CODE_DISABLE_1M_CONTEXT",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
"CLAUDE_CODE_ATTRIBUTION_HEADER",
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
"API_TIMEOUT_MS",
]
TIERS = ["opus", "sonnet", "haiku", "fable"]
# ---------------------------------------------------------------------------
# Providers
#
# Every gateway provider is described in providers.json - endpoint, auth, how
# its model list is fetched, how setup runs, which doctor checks apply, how it
# is drawn - so adding one that reuses an existing behaviour is an entry there
# rather than code in four places. What differs in *kind* (parsing a catalogue
# format, probing a server) stays in code, picked by name from the entry.
#
# anthropic is not in it: it is the native login, not a gateway.
#
# The file sits one level above this script in both layouts - the repository
# (linux/cm-json.py) and an install (~/.claude-mode/bin/cm-json.py) - so no
# path has to be passed around. CM_PROVIDERS overrides it, for tests.
# ---------------------------------------------------------------------------
PROVIDERS_PATH = os.environ.get("CM_PROVIDERS") or os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "providers.json")
_PROVIDERS = None
def providers():
global _PROVIDERS
if _PROVIDERS is None:
if not os.path.exists(PROVIDERS_PATH):
raise SystemExit("providers.json not found at %s - re-run the installer" % PROVIDERS_PATH)
data = load(PROVIDERS_PATH, {})
items = data.get("providers") if isinstance(data, dict) else None
_PROVIDERS = [p for p in (items or []) if isinstance(p, dict) and p.get("id")]
return _PROVIDERS
def provider(pid):
for p in providers():
if p["id"] == pid:
return p
return None
def _dig(node, dotted, default=None):
for part in dotted.split("."):
if isinstance(node, dict) and part in node:
node = node[part]
else:
return default
return node
def catalogue_kind(pid):
return _dig(provider(pid) or {}, "catalogue.kind", "")
def per_server_catalogue(pid):
"""A catalogue that belongs to one server rather than to the provider -
two LM Studio presets can point at two machines with different models."""
return bool(_dig(provider(pid) or {}, "catalogue.perServer", False))
def builtin_default_preset():
"""What `claude-mode <provider>` picks when no preset is named and none has
been chosen. Mirrored by builtin_default_preset() in the CLI."""
return {p["id"]: p.get("defaultPreset") or p["id"] for p in providers()}
# Column order of `provider-tsv`, which the shell reads once per run and looks
# rows up in (bash 3.2 on macOS has no associative arrays). Append only: the
# shell addresses these by number.
PROVIDER_TSV = [
("id", lambda p: p["id"]),
("aliases", lambda p: ",".join(p.get("aliases") or [])),
("title", lambda p: p.get("title") or p["id"]),
("label", lambda p: p.get("label") or p.get("title") or p["id"]),
("color", lambda p: p.get("color") or "gray"),
("defaultPreset", lambda p: p.get("defaultPreset") or p["id"]),
("serverEditable", lambda p: "1" if _dig(p, "server.editable") else "0"),
("probe", lambda p: _dig(p, "server.probe", "none")),
("probePaths", lambda p: ",".join(_dig(p, "server.paths", []) or [])),
("catalogueKind", lambda p: _dig(p, "catalogue.kind", "")),
("perServer", lambda p: "1" if _dig(p, "catalogue.perServer") else "0"),
("setupKey", lambda p: _dig(p, "setup.key", "required")),
("setupModels", lambda p: _dig(p, "setup.models", "per-tier")),
("keyUrl", lambda p: _dig(p, "setup.keyUrl", "")),
("guardrail", lambda p: "1" if p.get("guardrail") else "0"),
("doctor", lambda p: ",".join(p.get("doctor") or [])),
("defaultKeyRef", lambda p: _dig(p, "preset.auth.keyRef") or p["id"]),
("literalToken", lambda p: _dig(p, "preset.auth.token") or p["id"]),
("defaultBaseUrl", lambda p: _dig(p, "preset.baseUrl", "")),
("serverHint", lambda p: _dig(p, "server.hint", "")),
("serverStart", lambda p: _dig(p, "server.start", "")),
]
def cmd_provider_static(argv):
"""provider-static <id> - a fixed catalogue from providers.json as id<TAB>note."""
for m in _dig(provider(argv[0]) or {}, "catalogue.static", []) or []:
if isinstance(m, dict) and m.get("id"):
print("%s\t%s" % (m["id"], m.get("note", "")))
def cmd_ollama_ctx(argv):
"""ollama-ctx show | ps <model> (the matching Ollama response on stdin)
show: the model's own maximum, from /api/show's model_info
"<architecture>.context_length".
ps: the context a loaded model is actually running with, from /api/ps -
which is the number that matters, because Ollama sets it on the
server and cuts anything longer off without saying so.
Prints nothing when the answer is not there to read.
"""
try:
data = json.loads(sys.stdin.read() or "{}")
except ValueError:
return
if argv[0] == "show":
info = data.get("model_info") or {}
for k, v in info.items():
if k.endswith(".context_length") and isinstance(v, int):
print(v)
return
elif argv[0] == "ps" and len(argv) > 1:
# A bare name is `:latest` to Ollama, and /api/ps reports the tag.
want = {argv[1]} | ({argv[1] + ":latest"} if ":" not in argv[1] else set())
for m in data.get("models") or []:
if want & {m.get("name"), m.get("model")} and isinstance(m.get("context_length"), int):
print(m["context_length"])
return
def cmd_provider_tsv(argv):
"""provider-tsv - one tab-separated row per provider, columns as PROVIDER_TSV."""
for p in providers():
print("\t".join(str(fn(p)).replace("\t", " ") for _, fn in PROVIDER_TSV))
def cmd_provider_resolve(argv):
"""provider-resolve <word> - the provider id for an id or alias, else exit 1."""
word = (argv[0] if argv else "").strip().lower()
for p in providers():
if word == p["id"] or word in [a.lower() for a in (p.get("aliases") or [])]:
print(p["id"])
return
sys.exit(1)
def cmd_provider_get(argv):
"""provider-get <id> <dotted.path> - one value from a provider entry."""
p = provider(argv[0])
if p is None:
sys.exit(1)
val = _dig(p, argv[1])
if isinstance(val, (dict, list)):
print(json.dumps(val))
elif isinstance(val, bool):
print("true" if val else "false")
elif val is not None:
print(val)
def load(path, default=None):
if not os.path.exists(path):
return default if default is not None else {}
with open(path, "r", encoding="utf-8") as fh:
text = fh.read().strip()
if not text:
return default if default is not None else {}
return json.loads(text)
def save(path, data):
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
os.replace(tmp, path)
def cmd_apply(argv):
settings_path, state_path, mode = argv[0], argv[1], argv[2]
preset_path = argv[3] if len(argv) > 3 and argv[3] else None
helper = argv[4] if len(argv) > 4 and argv[4] else None
settings = load(settings_path)
state = load(state_path)
# Clear the baseline keys plus whatever the previous switch actually wrote,
# so a preset's custom extraEnv key cannot outlive the preset that added it.
doomed = set(BASE_MANAGED) | set(state.get("writtenEnvKeys") or [])
env = settings.get("env")
if isinstance(env, dict):
for k in doomed:
env.pop(k, None)
if not env:
settings.pop("env", None)
settings.pop("apiKeyHelper", None)
written = []
if mode != "anthropic":
if not preset_path:
raise SystemExit("apply: a gateway mode needs a preset")
preset = load(preset_path)
if preset.get("provider") != mode:
raise SystemExit(
"preset declares provider '%s', not '%s'" % (preset.get("provider"), mode)
)
models = preset.get("models") or {}
# Cost guard. Gateways resell Anthropic models at full list price with no
# subscription discount, so routing a tier there is almost never intended.
# Opt in per preset with "allowAnthropicModels": true.
if not preset.get("allowAnthropicModels"):
offenders = []
for tier in TIERS:
if is_anthropic_model(models.get(tier)):
offenders.append("%s -> %s" % (tier, models[tier]))
if is_anthropic_model(preset.get("subagentModel")):
offenders.append("subagent -> %s" % preset["subagentModel"])
if offenders:
raise SystemExit(
"refusing to switch: preset routes a tier at an Anthropic model "
"through '%s' (%s). Gateways bill these at full price. Add "
'"allowAnthropicModels": true to the preset if deliberate.'
% (mode, "; ".join(offenders))
)
block = {}
block["ANTHROPIC_BASE_URL"] = str(preset["baseUrl"])
# Explicitly empty, not absent: a cached Anthropic login can otherwise
# override the gateway config and surface as a model-not-found error.
# Removed entirely when switching back to anthropic.
block["ANTHROPIC_API_KEY"] = ""
for tier in TIERS:
val = models.get(tier)
if val:
block["ANTHROPIC_DEFAULT_%s_MODEL" % tier.upper()] = str(val)
if preset.get("subagentModel"):
block["CLAUDE_CODE_SUBAGENT_MODEL"] = str(preset["subagentModel"])
if preset.get("gatewayModelDiscovery"):
block["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1"
# Behind a custom base URL Claude Code cannot resolve a third-party model
# id to a context length, so it guesses low and auto-compacts early.
ctx = preset.get("contextTokens")
if ctx:
block["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = str(int(ctx))
block["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = str(int(ctx))
for k, v in (preset.get("extraEnv") or {}).items():
block[k] = str(v)
auth = preset.get("auth") or {"mode": "vault", "keyRef": "openrouter"}
if auth.get("mode") == "vault":
if not helper:
raise SystemExit("apply: vault auth needs the key-helper path")
# Claude Code runs this value as a shell command line, so a $HOME
# containing a space (common on macOS) has to arrive quoted or the
# shell splits it and tries to execute the first word. shlex.quote
# leaves an ordinary path untouched, so nothing churns.
settings["apiKeyHelper"] = shlex.quote(helper)
else:
block["ANTHROPIC_AUTH_TOKEN"] = str(auth.get("token") or "lmstudio")
env = settings.get("env")
if isinstance(env, dict):
env.update(block)
else:
settings["env"] = block
written = list(block.keys())
save(settings_path, settings)
state["mode"] = mode
state["preset"] = os.path.splitext(os.path.basename(preset_path))[0] if preset_path else ""
state["writtenEnvKeys"] = written
save(state_path, state)
for k in written:
print(k)
def cmd_summary(argv):
p = load(argv[0])
if p.get("description"):
print(p["description"])
models = p.get("models") or {}
bits = ["%s=%s" % (t, models[t]) for t in TIERS if models.get(t)]
if bits:
print(" ".join(bits))
ctx = p.get("contextTokens")
if ctx:
print("context {:,} tokens base {}".format(int(ctx), p.get("baseUrl", "")))
else:
print("base %s" % p.get("baseUrl", ""))
def cmd_models(argv):
p = load(argv[0])
models = p.get("models") or {}
for t in TIERS:
print("%s\t%s" % (t, models.get(t, "")))
print("subagent\t%s" % (p.get("subagentModel") or ""))
def cmd_set_tier(argv):
path, tier, model = argv[0], argv[1], argv[2]
p = load(path)
if tier == "subagent":
p["subagentModel"] = model
elif tier in TIERS:
p.setdefault("models", {})[tier] = model
else:
raise SystemExit("unknown tier '%s'" % tier)
save(path, p)
def cmd_scaffold(argv):
"""scaffold <provider> - a blank preset built from the provider's template."""
p = provider(argv[0])
if p is None:
raise SystemExit("unknown provider '%s'" % argv[0])
tpl = p.get("preset") or {}
base = {"provider": p["id"], "description": "new preset"}
base["baseUrl"] = tpl.get("baseUrl", "")
base["auth"] = dict(tpl.get("auth") or {"mode": "vault", "keyRef": p["id"]})
base["models"] = {t: "" for t in TIERS}
base["subagentModel"] = "inherit"
base["gatewayModelDiscovery"] = bool(tpl.get("gatewayModelDiscovery"))
base["contextTokens"] = int(tpl.get("contextTokens") or 200000)
if tpl.get("extraEnv"):
base["extraEnv"] = dict(tpl["extraEnv"])
print(json.dumps(base, indent=2))
def cmd_auth_of(argv):
"""auth-of <preset> - 'mode<TAB>keyRef' for the key helper, defaults filled in.
Exits non-zero when the file is missing, which load() would otherwise
hide as an empty preset - and an empty preset reads as vault/openrouter,
so a preset renamed mid-read would quietly hand out the wrong key.
"""
try:
with open(argv[0], encoding="utf-8") as fh:
text = fh.read().strip()
except OSError:
sys.exit(1)
p = json.loads(text) if text else {}
auth = p.get("auth") or {}
print("%s\t%s" % (auth.get("mode") or "vault", auth.get("keyRef") or "openrouter"))
def cmd_set_default(argv):
"""set-default <defaults.json> <provider> [name] - an empty name clears it."""
path, prov = argv[0], argv[1]
name = argv[2] if len(argv) > 2 else ""
try:
d = load(path, {})
except (OSError, ValueError):
d = {}
if not isinstance(d, dict):
d = {}
if name:
d[prov] = name
else:
d.pop(prov, None)
save(path, d)
print(json.dumps(d))
def cmd_preset_rename(argv):
"""preset-rename <presets-dir> <old> <new> <state.json> [defaults.json]
Every running session's key helper resolves the active preset through
state.json on each key fetch, so at no instant may state.json name a file
that is not there. The new name is linked in first, state.json repointed,
and only then does the old name go. A provider default chosen with
`preset default` follows the rename too. Prints {"renamed", "active",
"default"}.
"""
d, old, new, state_path = argv[:4]
defaults_path = argv[4] if len(argv) > 4 else ""
src = os.path.join(d, old + ".json")
dst = os.path.join(d, new + ".json")
if not os.path.isfile(src):
raise SystemExit("preset '%s' not found" % old)
if os.path.exists(dst):
raise SystemExit("preset '%s' already exists" % new)
try:
os.link(src, dst)
except OSError:
__import__("shutil").copy2(src, dst) # filesystems without hard links
state = load(state_path, {})
active = isinstance(state, dict) and state.get("preset") == old
if active:
state["preset"] = new
save(state_path, state)
moved = []
if defaults_path:
try:
defaults = load(defaults_path, {})
except (OSError, ValueError):
defaults = {}
if isinstance(defaults, dict):
moved = [p for p, n in defaults.items() if n == old]
if moved:
for p in moved:
defaults[p] = new
save(defaults_path, defaults)
os.unlink(src)
print(json.dumps({"renamed": True, "active": active, "default": bool(moved)}))
def cmd_get(argv):
data = load(argv[0])
cur = data
for part in argv[1].split("."):
if isinstance(cur, dict) and part in cur:
cur = cur[part]
else:
return
if isinstance(cur, (dict, list)):
print(json.dumps(cur))
elif isinstance(cur, bool):
print("true" if cur else "false")
elif cur is not None:
print(cur)
def cmd_presets(argv):
d = argv[0]
if not os.path.isdir(d):
return
for name in sorted(os.listdir(d)):
if not name.endswith(".json"):
continue
try:
p = load(os.path.join(d, name))
except Exception:
continue
print("%s\t%s\t%s" % (name[:-5], p.get("provider", "openrouter"), p.get("description", "")))
def cmd_managed(argv):
state = load(argv[0]) if argv else {}
for k in sorted(set(BASE_MANAGED) | set(state.get("writtenEnvKeys") or [])):
print(k)
def cmd_settings_env(argv):
"""Print 'KEY=VALUE' for every managed key currently in settings.json."""
settings = load(argv[0])
state = load(argv[1]) if len(argv) > 1 else {}
keys = set(BASE_MANAGED) | set(state.get("writtenEnvKeys") or [])
if settings.get("apiKeyHelper"):
print("apiKeyHelper=%s" % settings["apiKeyHelper"])
env = settings.get("env") or {}
for k in sorted(keys):
if k in env:
print("%s=%s" % (k, env[k]))
def cmd_or_models(argv):
"""OpenRouter /v1/models on stdin -> 'id<TAB>ctx<TAB>$in<TAB>$out' lines."""
data = json.loads(sys.stdin.read())
for m in sorted(data.get("data") or [], key=lambda x: x.get("id", "")):
pricing = m.get("pricing") or {}
def per_m(key):
try:
return round(float(pricing.get(key)) * 1e6, 3)
except (TypeError, ValueError):
return ""
print("%s\t%s\t%s\t%s" % (m.get("id", ""), m.get("context_length", ""), per_m("prompt"), per_m("completion")))
def cmd_cache_models(argv):
"""cache-models <cache.json> <provider> <1|0> [baseUrl] TSV catalogue on stdin
Keeps the last catalogue each provider returned, for a reader that cannot
afford a network round trip - the bar panel's model picker. One node per
provider with its own fetchedAt and ok, so a failed OpenRouter fetch never
invalidates a fresh LM Studio list. A failed fetch keeps the previous list
(a stale list beats none when the network blinks) and marks it ok=false.
Model ids, context lengths and prices only: never a key reference or token.
Prints the number of models stored by this call.
"""
path, provider, ok = argv[0], argv[1], argv[2] == "1"
base = argv[3].rstrip("/") if len(argv) > 3 else ""
kind = catalogue_kind(provider)
per_server = per_server_catalogue(provider)
def num(s, cast):
try:
return cast(s)
except (TypeError, ValueError):
return None
# The TSV columns are whatever that kind's parser prints (or-models,
# lms-models, ollama-models, openai-models, or a static list's id/note).
rows = []
for line in sys.stdin.read().splitlines():
parts = line.split("\t")
mid = parts[0].strip() if parts else ""
if not mid:
continue
m = {"id": mid}
if kind == "openrouter":
ctx, pin, pout = (parts[1:] + ["", "", ""])[:3]
m["contextTokens"] = num(ctx, int)
m["priceIn"] = num(pin, float)
m["priceOut"] = num(pout, float)
elif kind == "lmstudio":
state, ctx = (parts[1:] + ["", ""])[:2]
m["state"] = state
m["contextTokens"] = num(ctx, int)
elif kind == "ollama":
params, quant = (parts[1:] + ["", ""])[:2]
m["note"] = " ".join(x for x in (params, quant) if x and x != "-")
elif len(parts) > 1 and parts[1]:
m["note"] = parts[1]
rows.append(m)
try:
data = load(path, {})
except (OSError, ValueError):
data = {}
providers = data.get("providers") if isinstance(data, dict) else None
if not isinstance(providers, dict):
providers = {}
node = providers.get(provider)
if not isinstance(node, dict):
node = {}
now = __import__("datetime").datetime.now(
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
if ok:
node = {"fetchedAt": now, "ok": True, "models": rows}
if per_server:
node["baseUrl"] = base
else:
# A list from a different server is not stale, it is wrong.
if per_server and node.get("baseUrl", "") != base:
node = {"baseUrl": base, "models": []}
node["ok"] = False
node["failedAt"] = now
providers[provider] = node
save(path, {"schema": 1, "providers": providers})
print(len(rows) if ok else 0)
def cmd_lms_models(argv):
"""LM Studio /api/v0/models on stdin -> 'id<TAB>state<TAB>ctx' lines."""
data = json.loads(sys.stdin.read())
for m in sorted(data.get("data") or [], key=lambda x: x.get("id", "")):
print("%s\t%s\t%s" % (m.get("id", ""), m.get("state", "unknown"), m.get("max_context_length", "")))
def cmd_ollama_models(argv):
"""Ollama /api/tags on stdin -> 'id<TAB>parameters<TAB>quantization<TAB>family'.
The id is `name` (e.g. qwen3-coder:30b), which is what the Anthropic
endpoint accepts as a model. /api/tags carries no context length - that is
a server setting (OLLAMA_CONTEXT_LENGTH), not a property of the model file.
"""
data = json.loads(sys.stdin.read())
for m in sorted(data.get("models") or [], key=lambda x: x.get("name", "")):
d = m.get("details") or {}
# "-" rather than empty: bash's `read` with a tab IFS merges empty
# fields, which would slide the later columns left.
print("%s\t%s\t%s\t%s" % (m.get("name") or m.get("model", ""), d.get("parameter_size") or "-",
d.get("quantization_level") or "-", d.get("family") or "-"))
def cmd_openai_models(argv):
"""A `GET /v1/models` list on stdin -> one id per line.
Covers both the OpenAI shape and Anthropic's own ({"data": [{"id": ...}]}),
which is what a proxy in front of either tends to serve.
"""
data = json.loads(sys.stdin.read())
items = data.get("data") if isinstance(data, dict) else data
for m in sorted((items or []), key=lambda x: (x or {}).get("id", "")):
if isinstance(m, dict) and m.get("id"):
print(m["id"])
# A model id carrying a bracket suffix - e.g. `claude-fable-5[1m]` - is Claude
# Code's extended-context marker. It belongs to Anthropic's 1M models and no
# gateway recognises it. A session that had it can carry the tag onto a new id
# after a switch, producing something like
# `~deepseek/deepseek-v4-flash-latest[1m]` that only fails at compaction, because
# compaction re-resolves the model from session state.
TAGGED_MODEL_RE = re.compile(r"\[[0-9]+[a-zA-Z]\]$")
def walk_model_ids(node, out):
"""Collect model-ish strings anywhere in the config.
Claude Code caches resolved models under more than one key - both
clientDataCacheSlots and additionalModelOptionsCache have been observed - so
scan rather than reach for a fixed path.
"""
if isinstance(node, dict):
for k, v in node.items():
if isinstance(v, str):
if k in ("model", "value") and v and (v[0].isalnum() or v[0] == "~"):
out.add(v)
else:
walk_model_ids(v, out)
elif isinstance(node, list):
for item in node:
walk_model_ids(item, out)
def cmd_stale_models(argv):
"""Cached model ids worth flagging, as 'kind<TAB>id' lines.
kind is 'anthropic' (would bill at gateway list price) or 'tagged' (carries a
[1m]-style marker that breaks compaction on a gateway).
"""
ids = set()
walk_model_ids(load(argv[0], {}), ids)
for m in sorted(i for i in ids if is_anthropic_model(i)):
print("anthropic\t" + m)
for m in sorted(i for i in ids if TAGGED_MODEL_RE.search(i)):
print("tagged\t" + m)
def cmd_strip_tags(argv):
"""Remove [1m]-style suffixes from cached model ids.
The tag is NOT junk everywhere: on an Anthropic id it is how Claude Code
selects the 1M variant, so stripping it silently downgrades that choice to
200k. On a gateway id the same tag is meaningless and breaks compaction.
Default is gateway ids only; pass 'all' as argv[1] to include Anthropic ids.
Prints 'strip<TAB>id' and 'keep<TAB>id' lines.
"""
path = argv[0]
strip_all = len(argv) > 1 and argv[1] == "all"
raw = open(path, encoding="utf-8").read()
tagged = sorted(set(re.findall(r'"([^"]*\[[0-9]+[a-zA-Z]\])"', raw)))
if not tagged:
return
target = [t for t in tagged if strip_all or not is_anthropic_model(t)]
kept = [t for t in tagged if t not in target]
for k in kept:
print("keep\t" + k)
if not target:
return
fixed = raw
for t in target:
fixed = fixed.replace('"' + t + '"', '"' + re.sub(r"\[[0-9]+[a-zA-Z]\]$", "", t) + '"')
json.loads(fixed) # refuse to write anything that is not valid JSON
with open(path, "w", encoding="utf-8") as fh:
fh.write(fixed)
for t in target:
print("strip\t" + t)
def cmd_health(argv):
"""Write health.json - the machine-readable state a fleet reader consumes.
usage: health <root> <claude.json> <mode> <preset> <guardrail> <keyBackend> <version>
Contract, deliberately narrow:
* NO key material. keysConfigured is names only.
* Model lists are [{id, anthropic}] so a reader never re-derives the
Anthropic matcher. A tagged *Anthropic* id is normal (that is how the 1M
variant is selected) and must not render as a fault.
* guardrailStatus is tri-state or null; never collapse it to a boolean.
"""
root, cfg_path, mode, preset, guardrail, key_backend, version = (argv + [""] * 7)[:7]
ids = set()
walk_model_ids(load(cfg_path, {}), ids)
try:
raw = open(cfg_path, encoding="utf-8").read()
ids.update(re.findall(r'"([^"]*\[[0-9]+[a-zA-Z]\])"', raw))
except OSError:
pass
def entries(seq):
return [{"id": i, "anthropic": bool(is_anthropic_model(i))} for i in sorted(seq)]
h = {
"schema": 1,
"tool": "claude-mode",
"version": version or "0.0.0",
"updatedAt": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"os": "posix",
"mode": mode,
"preset": "" if mode == "anthropic" else preset,
}
tagged = entries(i for i in ids if TAGGED_MODEL_RE.search(i))
if mode == "anthropic":
h["staleModelIds"] = []
h["taggedModelIds"] = tagged
else:
p = load(os.path.join(root, "presets", preset + ".json"), {})
h["provider"] = p.get("provider", "")
h["baseUrl"] = p.get("baseUrl", "")
h["models"] = {t: p.get("models", {}).get(t, "") for t in TIERS if p.get("models", {}).get(t)}
h["subagentModel"] = p.get("subagentModel", "")
h["contextTokens"] = int(p["contextTokens"]) if p.get("contextTokens") else None
h["gatewayDiscovery"] = bool(p.get("gatewayModelDiscovery"))
auth = p.get("auth") or {}
if auth.get("mode", "vault") == "vault":
h["keyBackend"] = key_backend or "unknown"
h["keysConfigured"] = [auth.get("keyRef", "openrouter")]
else:
h["keyBackend"] = "inline"
h["keysConfigured"] = []
h["costGuardPassed"] = True
h["guardrailStatus"] = guardrail or None
h["staleModelIds"] = entries(i for i in ids if is_anthropic_model(i))
h["taggedModelIds"] = tagged
# The switchable catalogue, so a reader (the Omarchy bar widget) can offer
# every preset without re-walking the presets directory itself. Names,
# providers, descriptions and model maps only - the same shape already
# published for the active preset, and no key material rides along.
catalogue = []
pdir = os.path.join(root, "presets")
if os.path.isdir(pdir):
for fname in sorted(os.listdir(pdir)):
if not fname.endswith(".json"):
continue
pp = load(os.path.join(pdir, fname), {})
if not isinstance(pp, dict):
continue
pauth = pp.get("auth") or {}
pmode = pauth.get("mode", "vault")
catalogue.append({
"name": fname[:-5],
"provider": pp.get("provider", "openrouter"),
"description": pp.get("description", ""),
"contextTokens": int(pp["contextTokens"]) if pp.get("contextTokens") else None,
"models": {t: pp.get("models", {}).get(t, "")
for t in TIERS if pp.get("models", {}).get(t)},
# Enough for a reader to render an editing form without opening
# the preset file: where the server is, and whether it is set to
# send a real credential. The credential itself never appears -
# only which named slot it would come from.
"baseUrl": pp.get("baseUrl", ""),
"authMode": pmode,
"keyRef": pauth.get("keyRef", "") if pmode == "vault" else "",
})
h["presets"] = catalogue
# Which preset `claude-mode <provider>` picks when none is named, in the
# CLI's own order (default_preset_for, then resolve_preset): the chosen one
# while its file exists, else the built-in name if that exists, else the
# first by name. `defaultPresetChosen` holds only the explicit choices, so a
# reader can tell a choice it could clear from the built-in fallback.
names = {c["name"] for c in catalogue}
try:
chosen_raw = load(os.path.join(root, "defaults.json"), {})
except (OSError, ValueError):
chosen_raw = {}
if not isinstance(chosen_raw, dict):
chosen_raw = {}
effective, chosen = {}, {}
for prov, builtin in builtin_default_preset().items():
pick = chosen_raw.get(prov)
if pick and pick in names:
effective[prov] = chosen[prov] = pick
elif builtin in names:
effective[prov] = builtin
else:
mine = sorted(c["name"] for c in catalogue if c["provider"] == prov)
effective[prov] = mine[0] if mine else ""
h["defaultPresetFor"] = effective
h["defaultPresetChosen"] = chosen
# The providers as the widget needs them, in menu order: how to draw each
# one and which panel features apply. With this published, a provider
# added to providers.json shows up in the bar without touching its QML -
# or restarting the shell, which a change to Modes.js would need.
h["providers"] = [{
"id": p["id"],
"title": p.get("title") or p["id"],
"blurb": p.get("blurb", ""),
"logo": p.get("logo", ""),
"logoScale": float(p.get("logoScale") or 1.0),
"glyph": p.get("glyph", ""),
"serverEditable": bool(_dig(p, "server.editable")),
"serverHint": _dig(p, "server.hint", ""),
"defaultBaseUrl": _dig(p, "preset.baseUrl", ""),
"perServerCatalogue": bool(_dig(p, "catalogue.perServer")),
"defaultKeyRef": _dig(p, "preset.auth.keyRef") or p["id"],
"keyOptional": _dig(p, "setup.key", "required") != "required",
} for p in providers()]
save(os.path.join(root, "health.json"), h)
def cmd_preflight_json(argv):
"""Emit a preflight verdict as JSON.
usage: preflight-json <ok|blocked> <mode> <preset> <code> <title> <detail>
<remedy> <remedyKind> <keyRef> <baseUrl>
The shell side does the checking; this exists so the strings reach a reader
correctly quoted rather than through hand-rolled escaping in bash.
"""
(status, mode, preset, code, title, detail,
remedy, kind, key_ref, base_url) = (argv + [""] * 10)[:10]
out = {
"ok": status == "ok",
"mode": mode,
"preset": preset,
"code": code or ("ok" if status == "ok" else "blocked"),
"title": title,
"detail": detail,
"remedy": remedy,
"remedyKind": kind,
"keyRef": key_ref,
"baseUrl": base_url,
}
print(json.dumps(out))
def cmd_sessions_json(argv):
"""Convert the session TSV on stdin to JSON.
Columns, in order: pid, ppid, tty, busy, cwd, self, parentCmd. `busy` is a
sampled-CPU heuristic, not a promise, and is reported as such.
"""
rows = []
for line in sys.stdin.read().splitlines():
if not line.strip():
continue
parts = line.split("\t")
parts += [""] * (7 - len(parts))
pid, ppid, tty, busy, cwd, is_self, pcmd = parts[:7]
try:
pid_i = int(pid)
except ValueError:
continue
rows.append({
"pid": pid_i,
"ppid": int(ppid) if ppid.isdigit() else 0,
"tty": tty,
"busy": busy == "yes",
"cwd": cwd,
"self": is_self == "yes",
"parentCmd": pcmd,
})
print(json.dumps({"count": len(rows), "busy": sum(1 for r in rows if r["busy"]),
"sessions": rows}))
def cmd_set_url(argv):
"""set-url <preset> <baseUrl>
LM Studio is not necessarily on this machine. It can be another box on the
LAN, or something reached through a tunnel or a reverse proxy, so the base
URL is editable rather than fixed at the loopback address it ships with.
"""
path, url = argv[0], argv[1].strip().rstrip("/")
if not re.match(r"^https?://[^\s/]+", url):
raise SystemExit("base URL must start with http:// or https://")
p = load(path)
p["baseUrl"] = url
save(path, p)
print(url)
def cmd_set_auth(argv):
"""set-auth <preset> none|key [keyRef]
`none` is LM Studio's out-of-the-box state: it accepts any token, so the
literal placeholder is written inline and is explicitly not a secret. `key`
is for a server with authentication switched on, where the token is a real
credential and belongs in the vault like every other one - settings.json
never sees it either way.
"""
path, mode = argv[0], argv[1]
p = load(path)
# The provider's own names, not LM Studio's: an Ollama preset switched to
# "none" sends Ollama's placeholder, and one switched to "key" defaults to
# a vault slot named after its provider.
pid = p.get("provider", "")
key_ref = argv[2] if len(argv) > 2 and argv[2] else (
_dig(provider(pid) or {}, "preset.auth.keyRef") or pid or "lmstudio")
if mode == "none":
token = _dig(provider(pid) or {}, "preset.auth.token") or pid or "lmstudio"
p["auth"] = {"mode": "literal", "token": token}
elif mode == "key":
p["auth"] = {"mode": "vault", "keyRef": key_ref}
else:
raise SystemExit("auth mode must be 'none' or 'key'")
save(path, p)
print(json.dumps(p["auth"]))
def cmd_set_flag(argv):
"""set-flag <preset> <key> true|false
Only used for `configured` so far. Kept generic because a preset-level
boolean written by hand is exactly the kind of thing that ends up as a
string "false", which is truthy everywhere that matters.
"""
path, key, val = argv[0], argv[1], argv[2]
p = load(path)
p[key] = (val == "true")
save(path, p)
def cmd_set_all(argv):
"""set-all <preset> <model-id> - point every tier and the subagent at one id."""
path, model = argv[0], argv[1]
p = load(path)
p["models"] = {t: model for t in TIERS}
if p.get("subagentModel") and p["subagentModel"] != "inherit":
p["subagentModel"] = model
save(path, p)
print(model)
# ---------------------------------------------------------------------------
# Session transcript repair
#
# Native Anthropic requires previous_message_id to be an id it issued itself -
# one starting `msg_`. A session that took even one completion from a gateway
# while the mode was switched under it has that provider's id format in its
# transcript instead (OpenRouter issues `gen-<epoch>-<rand>`), and every attempt
# to resume it afterwards fails with a 400 naming previous_message_id. Client-
# side error placeholders, written as model `<synthetic>` with a UUID for an id,
# do the same thing when one is last.
#
# The transcript is newline-delimited JSON, one independent object per line, so
# rolling back to the last message Anthropic actually issued is a truncation.
# Everything after it is lost - which is the cost, and why nothing here runs
# without being asked twice.
# ---------------------------------------------------------------------------
def _msg_id(obj):
m = obj.get("message")
if isinstance(m, dict) and m.get("id"):
return str(m["id"])
return None
def _blocks(msg):
c = msg.get("content")
if isinstance(c, str):
return [{"type": "text", "text": c}]
return c if isinstance(c, list) else []
def _clip(t, n):
t = " ".join(str(t).split())
return t if len(t) <= n else t[:n - 1] + "\u2026"
def summarise_dropped(lines, per_block=1400):
"""Render the turns a repair is about to discard as readable Markdown.
Tool *results* are deliberately left out. They are the bulk of a transcript
by volume and the least useful part of a summary - what matters on the way
back in is what was asked, what was said, and what was run.
"""
out, tools = [], 0
for line in lines:
if not line.strip():
continue
try:
d = json.loads(line)
except Exception:
continue
t, msg = d.get("type"), (d.get("message") or {})
if t == "user" and not d.get("isMeta"):
for b in _blocks(msg):
if b.get("type") == "text" and b.get("text", "").strip():
out.append("### User\n\n" + _clip(b["text"], per_block))
elif t == "assistant":
if d.get("isApiErrorMessage"):
for b in _blocks(msg):
if b.get("type") == "text":
out.append("> **API error:** " + _clip(b.get("text", ""), 300))
continue
said, ran = [], []
for b in _blocks(msg):
if b.get("type") == "text" and b.get("text", "").strip():
said.append(_clip(b["text"], per_block))
elif b.get("type") == "tool_use":
inp = b.get("input") or {}
hint = inp.get("command") or inp.get("file_path") or inp.get("pattern") or inp.get("path") or ""
ran.append("`%s`%s" % (b.get("name", "tool"),
(" \u2014 " + _clip(hint, 120)) if hint else ""))
tools += 1
if said:
out.append("### Claude\n\n" + "\n\n".join(said))
if ran:
out.append("Ran: " + ", ".join(ran[:12]) + (" \u2026" if len(ran) > 12 else ""))
return "\n\n".join(out), tools
def _meta_entry(template, parent_uuid, text):
"""A user-role entry marked isMeta, which Claude Code treats as context
rather than as something to answer - the same marker it uses for its own
local-command caveats. It carries no message.id, so it cannot affect the
previous_message_id that made the session unresumable in the first place.
"""
now = __import__("datetime").datetime.now(
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
e = {
"parentUuid": parent_uuid,
"isSidechain": False,
"userType": template.get("userType", "external"),
"cwd": template.get("cwd", ""),
"sessionId": template.get("sessionId", ""),
"version": template.get("version", ""),
"gitBranch": template.get("gitBranch", ""),
"type": "user",
"isMeta": True,
"uuid": str(__import__("uuid").uuid4()),
"timestamp": now,
"message": {"role": "user", "content": text},
}
return {k: v for k, v in e.items() if v != "" or k in ("gitBranch",)}
def cmd_repair_session(argv):
"""repair-session <transcript.jsonl> [--apply] [--no-reinject]
Prints a JSON verdict. With --apply, writes the dropped turns out as
Markdown, backs the file up, truncates it to the last Anthropic-issued
message, and (unless --no-reinject) appends those turns back as a single
isMeta note so the resumed session still knows what it did.
"""
path = argv[0]
apply_it = "--apply" in argv[1:]
reinject = "--no-reinject" not in argv[1:]
with open(path, encoding="utf-8") as fh:
lines = fh.read().splitlines()
last_good = -1 # index of the last line carrying a msg_ id
foreign, synthetic = [], []
for i, line in enumerate(lines):
if not line.strip():
continue
try:
obj = json.loads(line)
except Exception:
continue
mid = _msg_id(obj)
if mid is None:
continue
if mid.startswith("msg_"):
last_good = i
elif mid.startswith("gen-"):
foreign.append({"line": i + 1, "id": mid,
"model": str((obj.get("message") or {}).get("model", ""))})
else:
synthetic.append({"line": i + 1, "id": mid,
"apiError": bool(obj.get("isApiErrorMessage"))})
# Only the *last* id matters for resuming: an error placeholder in the
# middle of a long-finished turn is history, not a blocker.
tail_ids = [i for i in range(len(lines) - 1, last_good, -1)
if lines[i].strip() and _msg_id(_safe(lines[i])) is not None]
healthy = (last_good >= 0 and not tail_ids)
# Not every transcript without an Anthropic id is damaged, and saying so
# turns the scan into noise. A session that ran start to finish on a gateway
# has `gen-` ids throughout by design: it resumes perfectly well under the
# provider it was born on, has nothing to truncate back to, and is only a
# problem if you try to resume it as Anthropic. Likewise a session that never
# got a reply at all has no ids and nothing wrong with it.
if healthy:
kind = "healthy"
elif last_good >= 0:
kind = "repairable"
elif foreign:
kind = "gateway-native"
elif synthetic:
kind = "synthetic-only"
else:
kind = "no-messages"
out = {
"path": path,
"lines": len(lines),
"lastGoodLine": last_good + 1 if last_good >= 0 else 0,
"dropLines": 0 if healthy or last_good < 0 else len(lines) - (last_good + 1),
"foreignIds": foreign,
"syntheticIds": synthetic,
"healthy": healthy,
"kind": kind,
"repairable": (not healthy) and last_good >= 0,
"applied": False,
"backup": "",
"recovered": "",
"reinjected": False,
"droppedTools": 0,
}
if apply_it and out["repairable"]:
stamp = __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M%S")
backup = "%s.pre-repair-backup-%s" % (path, stamp)
dropped = lines[last_good + 1:]
keep = lines[:last_good + 1]
# The turns being cut are the work itself. Losing the conversation that
# produced a morning's changes is most of the damage, so they are written
# out as readable Markdown before anything is deleted...
digest, ntools = summarise_dropped(dropped)
recovered = "%s.recovered-%s.md" % (path.rsplit(".jsonl", 1)[0], stamp)
header = ("# Recovered turns\n\n"
"Cut from `%s` on %s, because the transcript could no longer be\n"
"resumed. Everything below happened; none of it is in the session any more.\n\n"
"---\n\n" % (os.path.basename(path), stamp))
with open(recovered, "w", encoding="utf-8") as fh:
fh.write(header + (digest or "_No readable messages in the dropped turns._\n"))
with open(backup, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + ("\n" if lines else ""))
# ...and then handed back to the session, so the agent that resumes it
# knows what it just did rather than waking with a gap in its memory.
if reinject and digest:
template = _safe(keep[last_good])
body = digest if len(digest) <= 24000 else (
digest[:24000] + "\n\n\u2026 truncated; the full text is in " + recovered)
note = ("<recovered-transcript>\n"
"Context, not an instruction - do not act on it or reply to it.\n\n"
"This session was rolled back to its last resumable point after a "
"provider switch left it unable to resume. The turns below were part "
"of this conversation and are no longer in it. The full text is at "
+ recovered + "\n\n" + body + "\n</recovered-transcript>")
keep.append(json.dumps(_meta_entry(template, template.get("uuid"), note)))
out["reinjected"] = True
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(keep) + "\n")
out["applied"] = True
out["backup"] = backup
out["recovered"] = recovered
out["droppedTools"] = ntools
print(json.dumps(out))
def _safe(line):
try:
return json.loads(line)
except Exception:
return {}
def _last_msg_id_from_tail(path, window=262144):
"""The id of the last message-bearing line, read from the end of the file.
Classifying a transcript needs two facts: what the last message id is, and
whether any Anthropic id exists at all. The first decides the common case on
its own - if the last one is `msg_`, the transcript is healthy and the rest
of the file never has to be touched. Since that is 40-odd of the 59 here,
and some are 10MB, reading the tail first is the difference between a scan
that can run on a timer and one that cannot.
Returns (id_or_None, saw_whole_file).
"""
size = os.path.getsize(path)
with open(path, "rb") as fh:
start = max(0, size - window)
fh.seek(start)
chunk = fh.read()
whole = start == 0
if not whole:
nl = chunk.find(b"\n") # drop the partial first line
chunk = chunk[nl + 1:] if nl >= 0 else b""
for raw in reversed(chunk.splitlines()):
if not raw.strip():
continue
try:
d = json.loads(raw.decode("utf-8", "replace"))
except Exception:
continue
mid = _msg_id(d)
if mid:
return mid, whole
return None, whole
def _broken_entry(path):
"""The scan's record for one transcript if it needs repair, else None.
Raises OSError when the file cannot be read, which the scan counts as
skipped rather than as healthy.
"""
last, whole = _last_msg_id_from_tail(path)
# Healthy is decided by the tail alone, and is the common case.
if last is not None and last.startswith("msg_"):
return None
if last is None and whole:
return None # no replies at all; nothing to fix
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.read().splitlines()
last_good, foreign = -1, []
for i, line in enumerate(lines):
if not line.strip():
continue
try:
d = json.loads(line)
except Exception:
continue
mid = _msg_id(d)
if not mid:
continue
if mid.startswith("msg_"):
last_good = i
elif mid.startswith("gen-"):
foreign.append(str((d.get("message") or {}).get("model", "")))
if last_good < 0:
return None # gateway-native or reply-less
tail = [i for i in range(len(lines) - 1, last_good, -1)
if lines[i].strip() and _msg_id(_safe(lines[i])) is not None]
if not tail:
return None
return {
"sessionId": os.path.basename(path)[:-6],
"project": os.path.basename(os.path.dirname(path)),
"path": path,
"lines": len(lines),
"dropLines": len(lines) - (last_good + 1),
"providers": sorted(set(f for f in foreign if f)),
"mtime": int(os.path.getmtime(path)),
}
# ---------------------------------------------------------------------------
# Dismissed sessions
#
# Some damage is never going to be repaired - a throwaway session, or one whose
# work was finished another way - and without a way to say so it holds the
# bar's warning dot on forever, which teaches you to ignore the dot. So a broken
# session can be dismissed, and one untouched for longer than a threshold is
# hidden on its own. Neither is deleted or forgotten: both move to `ignored[]`,
# with the reason, and stay one command away.
#
# Its own file because nothing else can hold authored state: state.json is
# rewritten wholesale on every switch, health.json on every `health`.
# ---------------------------------------------------------------------------
IGNORE_AGE_DAYS = 7
SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]+$")
def _load_ignored(path):
try:
data = load(path, {})
except (OSError, ValueError):
data = {} # a corrupt file must not break the scan
sessions = data.get("sessions") if isinstance(data, dict) else None
return sessions if isinstance(sessions, dict) else {}
def _save_ignored(path, sessions):
save(path, {"schema": 1, "sessions": sessions})
def _find_transcript(projects, sid):
hits = sorted(glob.glob(os.path.join(projects, "*", sid + ".jsonl")))
return hits[0] if hits else None
def _prune_ignored(projects, sessions):
"""Drop entries whose transcript has gone, so a session id that is later
reused - or restored from a backup - does not come back already hidden."""
gone = [sid for sid in sessions if not _find_transcript(projects, sid)]
for sid in gone:
del sessions[sid]
return len(gone)
def cmd_ignore_session(argv):
"""ignore-session <ignored.json> <projects-dir> add|remove|clear|list [id]
Prints JSON. `list` reconciles every entry against the disk: whether the
transcript still exists, and whether it is still broken at all.
"""
path, projects, action = argv[0], argv[1], argv[2]
sid = argv[3] if len(argv) > 3 else ""
if sid.endswith(".jsonl"):
sid = sid[:-6]
if action in ("add", "remove") and not SESSION_ID_RE.match(sid):
raise SystemExit("not a session id: '%s'" % sid)
sessions = _load_ignored(path)
if action == "list":
rows = []
for key in sorted(sessions, key=lambda k: str(sessions[k].get("ignoredAt", ""))):
found = _find_transcript(projects, key)
try:
still = bool(found and _broken_entry(found))
except OSError:
still = False
rows.append({"sessionId": key,
"project": sessions[key].get("project", ""),
"ignoredAt": sessions[key].get("ignoredAt", ""),
"exists": bool(found), "broken": still})
print(json.dumps({"sessions": rows, "count": len(rows)}))
return
pruned = _prune_ignored(projects, sessions)
changed = False
if action == "add":
found = _find_transcript(projects, sid)
if not found:
raise SystemExit("no transcript for session '%s'" % sid)
if sid not in sessions:
sessions[sid] = {
"project": os.path.basename(os.path.dirname(found)),
"ignoredAt": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
changed = True
elif action == "remove":
changed = sessions.pop(sid, None) is not None
elif action == "clear":
changed = bool(sessions)
sessions = {}
else:
raise SystemExit("ignore-session: unknown action '%s'" % action)
if changed or pruned:
_save_ignored(path, sessions)
print(json.dumps({"action": action, "sessionId": sid, "changed": changed,
"pruned": pruned, "count": len(sessions)}))
def cmd_scan_sessions(argv):
"""scan-sessions <projects-dir> [max-age-days] [ignored.json]
JSON list of transcripts worth repairing. Only `repairable` is reported: a
transcript carrying a genuine Anthropic message with a different provider's
output after it. A session that ran entirely on a gateway, or never got a
reply, is not damage.
Broken transcripts that were dismissed, or not written to for longer than
max-age-days (default 7; 0 turns the age rule off), are reported under
`ignored[]` with a reason instead of under `broken[]`.
"""
root = argv[0]
age_arg = argv[1] if len(argv) > 1 else ""
try:
max_age = int(age_arg) if age_arg != "" else IGNORE_AGE_DAYS
except ValueError:
raise SystemExit("max age must be a whole number of days, not '%s'" % age_arg)
dismissed = _load_ignored(argv[2]) if len(argv) > 2 and argv[2] else {}
cutoff = __import__("time").time() - max_age * 86400 if max_age > 0 else None
broken, ignored, scanned, skipped = [], [], 0, 0
for proj in sorted(glob.glob(os.path.join(root, "*"))):
if not os.path.isdir(proj):
continue
for path in sorted(glob.glob(os.path.join(proj, "*.jsonl"))):
scanned += 1
try:
entry = _broken_entry(path)
except OSError:
skipped += 1
continue
if entry is None:
continue
# Dismissed outranks stale: it is the reason a person gave.
if entry["sessionId"] in dismissed:
entry["reason"] = "dismissed"
entry["ignoredAt"] = dismissed[entry["sessionId"]].get("ignoredAt", "")
ignored.append(entry)
elif cutoff is not None and entry["mtime"] < cutoff:
entry["reason"] = "stale"
ignored.append(entry)
else:
broken.append(entry)
print(json.dumps({"scanned": scanned, "skipped": skipped,
"broken": broken, "count": len(broken),
"ignored": ignored, "ignoredCount": len(ignored),
"maxAgeDays": max_age}))
COMMANDS = {
"health": cmd_health,
"preflight-json": cmd_preflight_json,
"sessions-json": cmd_sessions_json,
"repair-session": cmd_repair_session,
"scan-sessions": cmd_scan_sessions,
"ignore-session": cmd_ignore_session,
"stale-models": cmd_stale_models,
"strip-tags": cmd_strip_tags,
"or-models": cmd_or_models,
"lms-models": cmd_lms_models,
"ollama-models": cmd_ollama_models,
"openai-models": cmd_openai_models,
"provider-tsv": cmd_provider_tsv,
"provider-resolve": cmd_provider_resolve,
"provider-get": cmd_provider_get,
"provider-static": cmd_provider_static,
"ollama-ctx": cmd_ollama_ctx,
"cache-models": cmd_cache_models,
"apply": cmd_apply,
"summary": cmd_summary,
"models": cmd_models,
"set-tier": cmd_set_tier,
"set-url": cmd_set_url,
"set-flag": cmd_set_flag,
"set-all": cmd_set_all,
"set-auth": cmd_set_auth,
"scaffold": cmd_scaffold,
"preset-rename": cmd_preset_rename,
"auth-of": cmd_auth_of,
"set-default": cmd_set_default,
"get": cmd_get,
"presets": cmd_presets,
"managed": cmd_managed,
"settings-env": cmd_settings_env,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
sys.exit("usage: cm-json.py <%s> ..." % "|".join(sorted(COMMANDS)))
try:
COMMANDS[sys.argv[1]](sys.argv[2:])
except SystemExit:
raise
except Exception as exc: # surface a clean message to the shell
sys.exit("cm-json: %s" % exc)