#!/usr/bin/env python3 """isep-doctor — 這台機器上每一個 Claude Code,到底有沒有把 ISEP 載進來(inkstone/ISEP#150) 為什麼要有這支: plugin 載入失敗時,ISEP 的閘**一支都不會跑**,而且**沒有任何閘能講出來**—— 講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。 為什麼一台機器要分開問(09-13 總管複驗 c6991 推翻了第一版): 同一台 Mac 上有**不只一個** Claude Code:CLI(例:/opt/homebrew/bin/claude 2.1.220) 與 Claude Desktop 內建的那一份(~/Library/Application Support/Claude/claude-code/<版本>/…,例:2.1.266)。 它們讀同一份 ~/.claude,**卻不一定得出同一個結論**: settings.json 的 extraKnownMarketplaces.inkstone.source 多了 path 時—— 2.1.266:整個 marketplace 被忽略 ⇒ isep@inkstone 報 "Marketplace inkstone not found" 2.1.220:照常載入 第一版只看「設定長得像會出事」就對整台機器判 ❌,在 2.1.220 上是假陽性。 判準(實際狀態,不是設定長相): 對每一個找得到的 Claude Code 各跑一次 `plugin list --json`,看它**自己**回報的 isep@inkstone: 沒有這一筆 ⇒ ❌ 沒安裝 帶 errors ⇒ ❌ 沒載入(照實轉述;若 settings 比已安裝的 marketplace 多了抓取欄位,附上原因與修法) 沒有 errors ⇒ ✅(即使設定裡有 path——那台實際載得進來就是載得進來) 跑不動/看不懂 ⇒ ◐ 查不了(讀不到不等於沒載入,也不等於有) 設定比對**只當解釋**,永遠不單獨判 ❌。 離開碼:0 全部 ✅|1 至少一個 ❌|2 沒有 ❌、但至少一個查不了(或一個 Claude Code 都找不到) 測試用環境變數: ISEP_DOCTOR_CLAUDE_BINS 用這份清單取代自動尋找;以 os.pathsep 分隔,每項 `名稱=可執行檔路徑` ISEP_DOCTOR_CLAUDE_HOME 代替 ~/.claude 迴歸測試:scripts/test-isep-doctor.sh(離線)/scripts/test-isep-doctor-live.sh(真的 Claude Code、隔離 HOME) """ import glob import json import os import re import shutil import subprocess import sys PLUGIN_ID = "isep@inkstone" MARKET = "inkstone" FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths") DESKTOP_GLOB = "~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude" home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude") def _ver_key(path): m = re.search(r"/claude-code/([0-9][0-9.]*)/", path) return tuple(int(x) for x in m.group(1).split(".") if x) if m else () def find_claudes(): """回傳 [(名稱, 路徑)]。Desktop 只取最新版本那一份(它啟動的就是那一份;舊資料夾可能殘留)。""" env = os.environ.get("ISEP_DOCTOR_CLAUDE_BINS") if env is not None: out = [] for item in env.split(os.pathsep): if "=" in item: name, path = item.split("=", 1) out.append((name, path)) return out out = [] cli = shutil.which("claude") if cli: out.append(("CLI", cli)) desk = sorted(glob.glob(os.path.expanduser(DESKTOP_GLOB)), key=_ver_key) # 數字排序:2.1.99 < 2.1.266 if desk: out.append(("Desktop", desk[-1])) return out def version_of(exe): try: r = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=30) return (r.stdout.strip().split() or ["?"])[0] except Exception: # noqa: BLE001 return "?" def ask(exe): """回傳 (狀態, 內容)。狀態:'list' 內容=plugin list 陣列;'err' 內容=查不了的原因。""" try: r = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=90) except Exception as e: # noqa: BLE001 return "err", f"執行失敗({e})" try: data = json.loads(r.stdout) if not isinstance(data, list): raise ValueError("不是陣列") return "list", data except Exception: # noqa: BLE001 tail = (r.stdout or r.stderr or "").strip().splitlines()[-1:] or ["(沒有輸出)"] return "err", f"plugin list --json 看不懂(exit={r.returncode}:{tail[0][:200]})" def load(p): try: return json.load(open(p)) except Exception: # noqa: BLE001 return {} declared = (load(os.path.join(home, "settings.json")).get("extraKnownMarketplaces") or {}).get(MARKET, {}).get("source") known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source") extra = [] if isinstance(declared, dict): extra = [k for k in FETCH_FIELDS if k in declared and (not isinstance(known, dict) or declared.get(k) != known.get(k))] claudes = find_claudes() if not claudes: print("◐ 查不了:這台找不到任何 Claude Code 可執行檔(CLI 與 Desktop 都沒有)") sys.exit(2) bad, unknown, extra_hit = [], [], False for name, exe in claudes: label = f"{name} {version_of(exe)}" state, data = ask(exe) if state == "err": unknown.append(name) print(f"◐ {label}:查不了——{data}(讀不到不等於沒載入)") print(f" {exe}") continue entry = next((p for p in data if p.get("id") == PLUGIN_ID), None) if entry is None: bad.append(name) print(f"❌ {label}:{PLUGIN_ID} 沒有安裝——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有") print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone") continue errors = entry.get("errors") or [] if not errors: print(f"✅ {label}:{PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')})") continue bad.append(name) print(f"❌ {label}:{PLUGIN_ID} {entry.get('version')} 沒有載入——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有") for e in errors: print(f" Claude Code 回報:{e}") if extra: extra_hit = True print(f" 可能原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}," "這一版把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略") if extra_hit: fields = ", ".join(repr(k) for k in extra) print("修法(一行,只拿掉上面點名的欄位;改完完全關掉 Claude Desktop/重開 CLI session 再跑一次本指令):") print(f" python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));" f"s=d['extraKnownMarketplaces']['{MARKET}']['source'];[s.pop(k,None) for k in ({fields},)];" "json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"") if bad: sys.exit(1) sys.exit(2 if unknown else 0)