loki-mode 11.2.0 → 11.2.2
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/skill-link-heal.sh +54 -0
- package/autonomy/loki +156 -1201
- package/autonomy/provider-offer.sh +4 -4
- package/autonomy/run.sh +11 -3
- package/bin/loki +22 -0
- package/bin/loki-postinstall.js +3 -1
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/KNOWN-ISSUES.md +0 -1
- package/docs/v10/ADOPTION-PLAN.md +2 -2
- package/docs/v10/DECISIONS.md +2 -0
- package/docs/v10/FAILURE-CLASSES.md +37 -1
- package/loki-ts/dist/loki.js +729 -704
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +252 -65
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-Bs4Vyd9R.js → AdminPage-C-iMLNxe.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dmf_rtmv.js → Avatar-C7VUvwsO.js} +1 -1
- package/web-app/dist/assets/{Badge-DCEdgEN8.js → Badge-tGvQEhnw.js} +1 -1
- package/web-app/dist/assets/{Button-BpGXXEPk.js → Button-_iHlH_yL.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-CzssVmim.js → CockpitPage-J_nNjbcR.js} +1 -1
- package/web-app/dist/assets/{ComparePage-BU4XtB9F.js → ComparePage-B05ui6Xt.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-Bkjf4KP2.js → ErrorBoundary-Bxex6DLn.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CcGMfaq7.js → EvidenceReceiptPanel-BsgWfdD1.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-D53leEFJ.js → GitHubIssuesPanel-DZBhZ8_X.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-UVsPtV3-.js → GitHubPRsPanel-BaxlAl6R.js} +1 -1
- package/web-app/dist/assets/{HomePage-D9wO-XTX.js → HomePage-mVJvzqbF.js} +2 -2
- package/web-app/dist/assets/{LoginPage-ChxBSele.js → LoginPage-B0zd4A_G.js} +1 -1
- package/web-app/dist/assets/{MagicPage-BqQDKX2L.js → MagicPage-DXLQ-y_R.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CA22NkPl.js → MetricsPage-CLS34Xvr.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-oYt5qdHQ.js → NotFoundPage-BXG8PbNV.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-tSAdI6A6.js → ProjectPage-D3kFFC0F.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-Dk22bicF.js → ProjectsPage-2bFn-gnC.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-CwDSvMn6.js → SettingsPage-CBM0ziWE.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-By3IMy9k.js → ShowcasePage-BTaXhjI3.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DcWNR28_.js → SystemSettingsPage-BrC2hyAr.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-CFawyYQg.js → TeamsPage-oqxdbVbY.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-D7pCD-63.js → TemplatesPage-CEbUIY21.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CT2v1Vm2.js → TerminalOutput-aIoWLbJA.js} +1 -1
- package/web-app/dist/assets/{activity-DW6jRbjj.js → activity-CPyi45gt.js} +1 -1
- package/web-app/dist/assets/{bell-CJOu9IIM.js → bell-BhaNBaJp.js} +1 -1
- package/web-app/dist/assets/{bot-dVt0Xw_X.js → bot-Da8REaEP.js} +1 -1
- package/web-app/dist/assets/{check-BVC4A8YK.js → check-DPj99ArM.js} +1 -1
- package/web-app/dist/assets/{chevron-left-Djgr5pg-.js → chevron-left-CkxonN0X.js} +1 -1
- package/web-app/dist/assets/{circle-alert-OE2reifn.js → circle-alert-C0JkB1y5.js} +1 -1
- package/web-app/dist/assets/{clock-BUUN5rWU.js → clock-COzFIiWi.js} +1 -1
- package/web-app/dist/assets/{cloud-CzPIksNz.js → cloud-bjkac4kn.js} +1 -1
- package/web-app/dist/assets/{code-xml-DlE7vlGY.js → code-xml-hlKlvXUJ.js} +1 -1
- package/web-app/dist/assets/{database-DUauMcIC.js → database-D555S5Km.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CBx9DWbM.js → dollar-sign-CVcApzcD.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-hTmaP9Mz.js → file-code-corner-UORaMIcO.js} +1 -1
- package/web-app/dist/assets/{file-plus-DimFEqtO.js → file-plus-ALJtDoI2.js} +1 -1
- package/web-app/dist/assets/{globe-BV7yxcQ_.js → globe-CPlrM_IC.js} +1 -1
- package/web-app/dist/assets/{hammer-CW6Ol_ju.js → hammer-qaGZHpja.js} +1 -1
- package/web-app/dist/assets/{index-83w75NC1.js → index-BcXljIAO.js} +3 -3
- package/web-app/dist/assets/{layers-DMAqCHxh.js → layers-BV8R9VUK.js} +1 -1
- package/web-app/dist/assets/{loader-circle-B-TJNPo5.js → loader-circle-6TlDS9Tu.js} +1 -1
- package/web-app/dist/assets/{lock-DbvZW7dV.js → lock-B9t6oWI0.js} +1 -1
- package/web-app/dist/assets/{package-BWFcV2kS.js → package-2gEUAtZJ.js} +1 -1
- package/web-app/dist/assets/{plus-CjPzASFv.js → plus-DXocxPjU.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DJL2nFq0.js → refresh-cw-D3dWHBd6.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Bq3vK-Tu.js → rotate-ccw-CvuWqaSB.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CAMgtDYH.js → scroll-text-ktmD_xJY.js} +1 -1
- package/web-app/dist/assets/{server-D71_Bywf.js → server-E7pZtj5A.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DqQ5wzQ6.js → shield-alert-968j9z0V.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DXE_IT36.js → thumbs-up-Tz_8rpU_.js} +1 -1
- package/web-app/dist/assets/{trash-2-VYAPKajq.js → trash-2-Drh9IIj8.js} +1 -1
- package/web-app/dist/assets/{trending-down-CuE-J2LH.js → trending-down-DCQXtVSL.js} +1 -1
- package/web-app/dist/assets/{trending-up-BvdD9XmQ.js → trending-up-BIiwQbWV.js} +1 -1
- package/web-app/dist/assets/{upload-OM2YFziE.js → upload-sCbPkfmy.js} +1 -1
- package/web-app/dist/assets/{usePolling-Bk39fp_d.js → usePolling-BMIg6ZVT.js} +1 -1
- package/web-app/dist/assets/{user-CWmKQURs.js → user-kGzXC3V2.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/autonomy/loki
CHANGED
|
@@ -1150,6 +1150,53 @@ except Exception:
|
|
|
1150
1150
|
PY
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
|
+
# loki_verify_root: the ONE place that decides which tree the receipt verifier
|
|
1154
|
+
# hashes (FC-29). The verifier walks and digests the directory it is given, so
|
|
1155
|
+
# an ambient cwd (a huge checkout, $HOME, a non-project directory) must never be
|
|
1156
|
+
# the default. Order: explicit TARGET_DIR; the parent of an absolute LOKI_DIR;
|
|
1157
|
+
# the cwd if it holds .loki; the repo top-level if it holds .loki. Anything else
|
|
1158
|
+
# prints one plain sentence on stderr and returns 1 (callers report NOT CHECKED).
|
|
1159
|
+
loki_verify_root() {
|
|
1160
|
+
# Helpers live inside so callers that extract this one function stay whole.
|
|
1161
|
+
_loki_verify_trim() {
|
|
1162
|
+
local p="$1"
|
|
1163
|
+
while [ "${#p}" -gt 1 ] && [ "${p%/}" != "$p" ]; do p="${p%/}"; done
|
|
1164
|
+
printf '%s' "$p"
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
_loki_verify_canon() {
|
|
1168
|
+
local p r
|
|
1169
|
+
p="$(_loki_verify_trim "$1")"
|
|
1170
|
+
if [ -n "$p" ] && [ -d "$p" ] && r="$(cd "$p" 2>/dev/null && pwd -P)"; then p="$r"; fi
|
|
1171
|
+
printf '%s' "$p"
|
|
1172
|
+
}
|
|
1173
|
+
local d="${TARGET_DIR:-}" top="" _cd _ch
|
|
1174
|
+
d="$(_loki_verify_trim "$d")"
|
|
1175
|
+
if [ -z "$d" ]; then
|
|
1176
|
+
case "${LOKI_DIR:-}" in
|
|
1177
|
+
/*) [ -d "$LOKI_DIR" ] && d="$(dirname "$LOKI_DIR")" ;;
|
|
1178
|
+
esac
|
|
1179
|
+
fi
|
|
1180
|
+
if [ -z "$d" ]; then
|
|
1181
|
+
if [ -d "$PWD/.loki" ]; then
|
|
1182
|
+
d="$PWD"
|
|
1183
|
+
else
|
|
1184
|
+
top="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
1185
|
+
if [ -n "$top" ] && [ -d "$top/.loki" ]; then d="$top"; fi
|
|
1186
|
+
fi
|
|
1187
|
+
fi
|
|
1188
|
+
# Compare canonical paths: every trailing slash stripped, symlinks resolved,
|
|
1189
|
+
# identically for the candidate and HOME (mirrored by resolveVerifyRoot in
|
|
1190
|
+
# loki-ts/src/commands/proof.ts).
|
|
1191
|
+
_cd="$(_loki_verify_canon "$d")"
|
|
1192
|
+
_ch="$(_loki_verify_canon "${HOME:-}")"
|
|
1193
|
+
if [ -z "$d" ] || [ "$_cd" = "/" ] || { [ -n "$_ch" ] && [ "$_cd" = "$_ch" ]; }; then
|
|
1194
|
+
echo "Cannot tell which project tree to verify against: set TARGET_DIR or run from a project directory that contains .loki." >&2
|
|
1195
|
+
return 1
|
|
1196
|
+
fi
|
|
1197
|
+
printf '%s\n' "$d"
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1153
1200
|
loki_remote_verify_receipt() {
|
|
1154
1201
|
local pj="$1"
|
|
1155
1202
|
local verifier="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"
|
|
@@ -1181,7 +1228,9 @@ loki_remote_verify_receipt() {
|
|
|
1181
1228
|
# empty for a local receipt, which simply means no attestation is checked.
|
|
1182
1229
|
local _attest_url="${2:-}" _attest=""
|
|
1183
1230
|
_attest="$(loki_remote_attestation_status "$pj" "$_attest_url")"
|
|
1184
|
-
|
|
1231
|
+
local _vroot
|
|
1232
|
+
_vroot="$(loki_verify_root)" || { echo " NOT VERIFIED: no project tree to check against" >&2; return 0; }
|
|
1233
|
+
out="$(python3 -E "$verifier" "$pj" "$_vroot" 2>/dev/null)"
|
|
1185
1234
|
hash_ok="$(printf '%s' "$out" | jq -r '.hash_ok // false' 2>/dev/null)"
|
|
1186
1235
|
gpg_ok="$(printf '%s' "$out" | jq -r '.gpg_ok | tostring' 2>/dev/null)"
|
|
1187
1236
|
|
|
@@ -9547,8 +9596,9 @@ _deploy_receipt_verdict() {
|
|
|
9547
9596
|
printf '%s' "UNCHECKED"; return 0
|
|
9548
9597
|
fi
|
|
9549
9598
|
|
|
9550
|
-
local out hash_ok
|
|
9551
|
-
|
|
9599
|
+
local out hash_ok _vroot
|
|
9600
|
+
_vroot="$(loki_verify_root 2>/dev/null)" || { printf '%s' "UNCHECKED"; return 0; }
|
|
9601
|
+
out="$(python3 -E "$verifier" "$pj" "$_vroot" 2>/dev/null || true)"
|
|
9552
9602
|
|
|
9553
9603
|
# "The verifier did not RUN" and "the verifier says the hash is bad" are
|
|
9554
9604
|
# different facts and must not collapse. If they do, a broken python3
|
|
@@ -13895,1226 +13945,127 @@ print(json.dumps({
|
|
|
13895
13945
|
return 0
|
|
13896
13946
|
}
|
|
13897
13947
|
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
echo " required egress remains."
|
|
13924
|
-
echo ""
|
|
13925
|
-
echo "Checks: node, python3, jq, git, curl, bash version,"
|
|
13926
|
-
echo " provider CLIs, disk space, and cockpit render capability."
|
|
13927
|
-
return 0
|
|
13928
|
-
;;
|
|
13929
|
-
*)
|
|
13930
|
-
echo -e "${RED}Unknown option: $1${NC}"
|
|
13931
|
-
echo "Usage: loki doctor [--json] [--airgap]"
|
|
13932
|
-
return 1
|
|
13933
|
-
;;
|
|
13934
|
-
esac
|
|
13935
|
-
done
|
|
13936
|
-
|
|
13937
|
-
if [ "$airgap_audit" = true ]; then
|
|
13938
|
-
_loki_airgap_audit "$json_output"
|
|
13939
|
-
return $?
|
|
13940
|
-
fi
|
|
13941
|
-
|
|
13942
|
-
if [ "$json_output" = true ]; then
|
|
13943
|
-
cmd_doctor_json
|
|
13944
|
-
return $?
|
|
13945
|
-
fi
|
|
13946
|
-
|
|
13947
|
-
echo -e "${BOLD}Loki Mode Doctor${NC}"
|
|
13948
|
-
echo ""
|
|
13949
|
-
echo "Checking system prerequisites..."
|
|
13950
|
-
echo ""
|
|
13951
|
-
|
|
13952
|
-
local pass_count=0
|
|
13953
|
-
local fail_count=0
|
|
13954
|
-
# Set to 1 when the bundled SDK is the ONLY usable provider (no CLI on PATH).
|
|
13955
|
-
# Declared here, not inside the branch that sets it, so the "Next:" block at
|
|
13956
|
-
# the end of this function always reads an initialized value.
|
|
13957
|
-
local _doctor_sdk_only=0
|
|
13958
|
-
# FUNNEL FIX (v8.2.x): collect the NAME + FIX of every hard failure, not just
|
|
13959
|
-
# a count. A first-time user on a bare machine sees 15 warnings (sentrux, GPG
|
|
13960
|
-
# signing, bash 4, Bun, python3.12, truecolor, inline-image probe -- all
|
|
13961
|
-
# OPTIONAL) around 2 real blockers, then a summary that says only "Some
|
|
13962
|
-
# required prerequisites are missing" without naming them. That is the point
|
|
13963
|
-
# where someone closes the terminal. Naming the blockers and their exact
|
|
13964
|
-
# install command turns a wall of yellow into a two-line to-do.
|
|
13965
|
-
local _doctor_blockers=""
|
|
13966
|
-
_doctor_block() { _doctor_blockers="${_doctor_blockers}\n - $1"; }
|
|
13967
|
-
local warn_count=0
|
|
13968
|
-
|
|
13969
|
-
# Helper: check command exists and optionally check version
|
|
13970
|
-
doctor_check() {
|
|
13971
|
-
local name="$1"
|
|
13972
|
-
local cmd="$2"
|
|
13973
|
-
local required="$3" # required, recommended, optional
|
|
13974
|
-
local min_version="${4:-}"
|
|
13975
|
-
|
|
13976
|
-
if ! command -v "$cmd" &> /dev/null; then
|
|
13977
|
-
if [ "$required" = "required" ]; then
|
|
13978
|
-
echo -e " ${RED}FAIL${NC} $name - not found"
|
|
13979
|
-
_doctor_block "${name%% (*} is not installed"
|
|
13980
|
-
fail_count=$((fail_count + 1))
|
|
13981
|
-
elif [ "$required" = "recommended" ]; then
|
|
13982
|
-
echo -e " ${YELLOW}WARN${NC} $name - not found (recommended)"
|
|
13983
|
-
warn_count=$((warn_count + 1))
|
|
13984
|
-
else
|
|
13985
|
-
echo -e " ${YELLOW}WARN${NC} $name - not found (optional)"
|
|
13986
|
-
warn_count=$((warn_count + 1))
|
|
13987
|
-
fi
|
|
13988
|
-
return 1
|
|
13989
|
-
fi
|
|
13990
|
-
|
|
13991
|
-
local version=""
|
|
13992
|
-
case "$cmd" in
|
|
13993
|
-
node)
|
|
13994
|
-
version=$(node --version 2>/dev/null | tr -d 'v')
|
|
13995
|
-
;;
|
|
13996
|
-
python3)
|
|
13997
|
-
version=$(python3 --version 2>/dev/null | awk '{print $2}')
|
|
13998
|
-
;;
|
|
13999
|
-
git)
|
|
14000
|
-
version=$(git --version 2>/dev/null | awk '{print $3}')
|
|
14001
|
-
;;
|
|
14002
|
-
bash)
|
|
14003
|
-
version=$("$cmd" --version 2>/dev/null | head -1 | sed 's/.*version \([0-9.]*\).*/\1/')
|
|
14004
|
-
;;
|
|
14005
|
-
bun)
|
|
14006
|
-
version=$(bun --version 2>/dev/null | head -1 | tr -d 'v ')
|
|
14007
|
-
;;
|
|
14008
|
-
jq)
|
|
14009
|
-
version=$(jq --version 2>/dev/null | tr -d 'jq-')
|
|
14010
|
-
;;
|
|
14011
|
-
curl)
|
|
14012
|
-
version=$(curl --version 2>/dev/null | head -1 | awk '{print $2}')
|
|
14013
|
-
;;
|
|
14014
|
-
claude)
|
|
14015
|
-
version=$(claude --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
14016
|
-
;;
|
|
14017
|
-
codex)
|
|
14018
|
-
version=$(codex --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
14019
|
-
;;
|
|
14020
|
-
cline)
|
|
14021
|
-
version=$(cline --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
14022
|
-
;;
|
|
14023
|
-
aider)
|
|
14024
|
-
version=$(aider --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
14025
|
-
;;
|
|
14026
|
-
opencode)
|
|
14027
|
-
version=$(opencode --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
14028
|
-
;;
|
|
14029
|
-
esac
|
|
14030
|
-
|
|
14031
|
-
local version_display=""
|
|
14032
|
-
if [ -n "$version" ]; then
|
|
14033
|
-
version_display=" (v$version)"
|
|
14034
|
-
fi
|
|
14035
|
-
|
|
14036
|
-
# Simple major version check if min_version is specified
|
|
14037
|
-
if [ -n "$min_version" ] && [ -n "$version" ]; then
|
|
14038
|
-
local cur_major cur_minor min_major min_minor
|
|
14039
|
-
cur_major=$(echo "$version" | cut -d. -f1)
|
|
14040
|
-
min_major=$(echo "$min_version" | cut -d. -f1)
|
|
14041
|
-
cur_minor=$(echo "$version" | cut -d. -f2)
|
|
14042
|
-
min_minor=$(echo "$min_version" | cut -d. -f2)
|
|
14043
|
-
|
|
14044
|
-
if [ "$cur_major" -lt "$min_major" ] 2>/dev/null || \
|
|
14045
|
-
{ [ "$cur_major" -eq "$min_major" ] 2>/dev/null && [ "$cur_minor" -lt "$min_minor" ] 2>/dev/null; }; then
|
|
14046
|
-
if [ "$required" = "required" ]; then
|
|
14047
|
-
echo -e " ${RED}FAIL${NC} $name$version_display - requires >= $min_version"
|
|
14048
|
-
_doctor_block "${name%% (*} must be >= $min_version"
|
|
14049
|
-
fail_count=$((fail_count + 1))
|
|
14050
|
-
else
|
|
14051
|
-
echo -e " ${YELLOW}WARN${NC} $name$version_display - recommended >= $min_version"
|
|
14052
|
-
warn_count=$((warn_count + 1))
|
|
14053
|
-
fi
|
|
14054
|
-
return 1
|
|
14055
|
-
fi
|
|
14056
|
-
fi
|
|
14057
|
-
|
|
14058
|
-
echo -e " ${GREEN}PASS${NC} $name$version_display"
|
|
14059
|
-
pass_count=$((pass_count + 1))
|
|
14060
|
-
return 0
|
|
14061
|
-
}
|
|
14062
|
-
|
|
14063
|
-
echo -e "${CYAN}Required:${NC}"
|
|
14064
|
-
doctor_check "Node.js (>= 18)" node required 18.0 || true
|
|
14065
|
-
doctor_check "Python 3 (>= 3.8)" python3 required 3.8 || true
|
|
14066
|
-
doctor_check "jq" jq required || true
|
|
14067
|
-
doctor_check "git" git required || true
|
|
14068
|
-
doctor_check "curl" curl required || true
|
|
14069
|
-
echo ""
|
|
14070
|
-
|
|
14071
|
-
echo -e "${CYAN}AI Providers:${NC}"
|
|
14072
|
-
# C1 (doctor UX): when an individual provider CLI is missing, print the
|
|
14073
|
-
# EXACT install command right under the WARN line so the user never has to
|
|
14074
|
-
# look it up. doctor_check returns 1 when the CLI is absent; we capture that
|
|
14075
|
-
# and emit the canonical install command for that provider. Honest: this
|
|
14076
|
-
# only PRINTS the command -- the offer-to-run path lives below, gated on a
|
|
14077
|
-
# TTY, and only when NO provider at all is installed.
|
|
14078
|
-
doctor_provider_install_cmd() {
|
|
14079
|
-
case "$1" in
|
|
14080
|
-
claude) echo "npm install -g @anthropic-ai/claude-code" ;;
|
|
14081
|
-
codex) echo "npm install -g @openai/codex" ;;
|
|
14082
|
-
cline) echo "npm install -g cline" ;;
|
|
14083
|
-
aider) echo "pip install aider-chat" ;;
|
|
14084
|
-
opencode) echo "npm install -g opencode-ai" ;;
|
|
14085
|
-
*) echo "" ;;
|
|
14086
|
-
esac
|
|
14087
|
-
}
|
|
14088
|
-
doctor_check_provider() {
|
|
14089
|
-
local _label="$1" _cmd="$2"
|
|
14090
|
-
if ! doctor_check "$_label" "$_cmd" optional; then
|
|
14091
|
-
local _install
|
|
14092
|
-
_install=$(doctor_provider_install_cmd "$_cmd")
|
|
14093
|
-
# Route the per-provider install hint to STDERR (fd 2), mirroring the
|
|
14094
|
-
# doctor_probe_note stderr-only pattern above. The Bun route emits the
|
|
14095
|
-
# same hint on STDOUT immediately after the provider WARN line; under
|
|
14096
|
-
# the parity harness's 2>&1 capture both land adjacent to the WARN, so
|
|
14097
|
-
# the two routes stay byte-identical when an optional provider is absent
|
|
14098
|
-
# (v7.63.0: closed the cline-absent parity gap by adding the Bun hint).
|
|
14099
|
-
[ -n "$_install" ] && echo -e " ${YELLOW}Install: ${_install}${NC}" >&2
|
|
14100
|
-
fi
|
|
14101
|
-
}
|
|
14102
|
-
doctor_check_provider "Claude CLI" claude || true
|
|
14103
|
-
doctor_check_provider "Codex CLI" codex || true
|
|
14104
|
-
doctor_check_provider "Cline CLI" cline || true
|
|
14105
|
-
doctor_check_provider "Aider CLI" aider || true
|
|
14106
|
-
doctor_check_provider "opencode CLI" opencode || true
|
|
14107
|
-
|
|
14108
|
-
# Check if at least one provider is installed (detect_any_provider is the
|
|
14109
|
-
# shared helper from provider-offer.sh, extracted from this exact loop).
|
|
14110
|
-
if ! detect_any_provider; then
|
|
14111
|
-
# The bundled Claude Agent SDK runs the main loop with no separate CLI,
|
|
14112
|
-
# but ONLY when it is genuinely usable: binary extracted, credentials
|
|
14113
|
-
# present, and the SDK loop actually active. detect_bundled_sdk_provider
|
|
14114
|
-
# fails closed on any doubt, so when it returns false we emit today's
|
|
14115
|
-
# blocker verbatim. Mirrored in loki-ts/src/commands/doctor.ts.
|
|
14116
|
-
if declare -f detect_bundled_sdk_provider >/dev/null 2>&1 && detect_bundled_sdk_provider; then
|
|
14117
|
-
# Say exactly which commands this covers. "No separate CLI needed" was
|
|
14118
|
-
# true for `loki start` and FALSE for demo/quick/quickstart, which stay
|
|
14119
|
-
# on the bash route and require a binary on PATH (provider-offer.sh:44
|
|
14120
|
-
# documents why folding the SDK predicate into detect_any_provider
|
|
14121
|
-
# would be a fail-open). The old wording produced the worst first-run
|
|
14122
|
-
# outcome we have: a green doctor ending in "Next: loki quickstart",
|
|
14123
|
-
# followed by quickstart, demo, quick and start-on-bash all exiting 2.
|
|
14124
|
-
echo -e " ${GREEN}PASS${NC} Bundled Claude Agent SDK is usable -- 'loki start' needs no separate CLI"
|
|
14125
|
-
echo -e " ${YELLOW}Note: loki demo/quick/quickstart still need a provider CLI on PATH${NC}"
|
|
14126
|
-
echo -e " ${YELLOW} Install: npm install -g @anthropic-ai/claude-code${NC}"
|
|
14127
|
-
# The note above was only half the fix. The final "Next:" line at the
|
|
14128
|
-
# bottom of this function recommended quickstart unconditionally, so a
|
|
14129
|
-
# user reading a GREEN doctor followed its LAST line straight into
|
|
14130
|
-
# quickstart.sh:476 exit 2. Record the state here (where the predicate
|
|
14131
|
-
# is already evaluated) and let the recommendation read it.
|
|
14132
|
-
_doctor_sdk_only=1
|
|
14133
|
-
pass_count=$((pass_count + 1))
|
|
14134
|
-
else
|
|
14135
|
-
echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
|
|
14136
|
-
echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
|
|
14137
|
-
_doctor_block "No AI provider CLI. Fix: npm install -g @anthropic-ai/claude-code"
|
|
14138
|
-
fail_count=$((fail_count + 1))
|
|
14139
|
-
# v7.29.0: on a TTY (non-json), append the consent-gated install offer.
|
|
14140
|
-
# In report mode the helper is a no-op on non-TTY/CI, so the doctor
|
|
14141
|
-
# output stays byte-identical for the bun-parity matrix. --json never
|
|
14142
|
-
# reaches here (cmd_doctor dispatches to cmd_doctor_json earlier).
|
|
14143
|
-
if declare -f offer_provider_install >/dev/null 2>&1; then
|
|
14144
|
-
offer_provider_install report || true
|
|
14145
|
-
fi
|
|
13948
|
+
# _loki_bun_delegate <cmd> [args...]
|
|
13949
|
+
# Hand a command to its single Bun implementation (FC-DUP, docs/v10/FAILURE-CLASSES.md).
|
|
13950
|
+
# Resolves bun the way bin/loki's _loki_bun does (PATH first, then an npm-installed
|
|
13951
|
+
# @oven/bun-* package, including bun.exe on Windows), resolves the CLI (dist, then
|
|
13952
|
+
# src), preflights `bun <cli> version` under a 5s timeout (when `timeout` exists),
|
|
13953
|
+
# then execs. On success it never returns. On failure it returns 1 and sets
|
|
13954
|
+
# _LOKI_DELEGATE_WHY (the single reason) and _LOKI_DELEGATE_FIX (one fix line) so
|
|
13955
|
+
# the caller can print a minimal diagnostic.
|
|
13956
|
+
_loki_bun_delegate() {
|
|
13957
|
+
local cmd="$1"
|
|
13958
|
+
shift
|
|
13959
|
+
local root="${_LOKI_SCRIPT_DIR}/.."
|
|
13960
|
+
local c bun_bin="" cli="" t="" rc=0 found=0
|
|
13961
|
+
_LOKI_DELEGATE_WHY=""
|
|
13962
|
+
_LOKI_DELEGATE_FIX=""
|
|
13963
|
+
|
|
13964
|
+
for c in "$(command -v bun 2>/dev/null || true)" \
|
|
13965
|
+
"$root"/node_modules/@oven/bun-*/bin/bun \
|
|
13966
|
+
"$root"/node_modules/@oven/bun-*/bin/bun.exe \
|
|
13967
|
+
"$root/node_modules/bun/bin/bun.exe"; do
|
|
13968
|
+
[ -n "$c" ] && [ -x "$c" ] || continue
|
|
13969
|
+
found=1
|
|
13970
|
+
if "$c" --version >/dev/null 2>&1; then
|
|
13971
|
+
bun_bin="$c"
|
|
13972
|
+
break
|
|
14146
13973
|
fi
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
# never reaches here: it only picks an installed provider. Bundled-SDK
|
|
14153
|
-
# claude stays allowed. Mirrored in loki-ts/src/commands/doctor.ts.
|
|
14154
|
-
if [ -n "${LOKI_PROVIDER:-}" ] && [ "$fail_count" -eq 0 ] \
|
|
14155
|
-
&& ! command -v "$LOKI_PROVIDER" >/dev/null 2>&1 \
|
|
14156
|
-
&& ! { [ "$LOKI_PROVIDER" = "claude" ] && [ "$_doctor_sdk_only" = "1" ]; }; then
|
|
14157
|
-
_sel_install=$(doctor_provider_install_cmd "$LOKI_PROVIDER")
|
|
14158
|
-
echo -e " ${RED}FAIL${NC} Selected provider '${LOKI_PROVIDER}' CLI not found"
|
|
14159
|
-
_doctor_block "Selected provider ${LOKI_PROVIDER} CLI not found. Fix: ${_sel_install:-install ${LOKI_PROVIDER}, or unset LOKI_PROVIDER}"
|
|
14160
|
-
fail_count=$((fail_count + 1))
|
|
14161
|
-
fi
|
|
14162
|
-
|
|
14163
|
-
# Provider Availability. Which provider a build would ACTUALLY pick, which
|
|
14164
|
-
# is decided by providers/loader.sh auto_detect_provider, not by PATH order
|
|
14165
|
-
# and not by the section above. Rendered by the shared helper in
|
|
14166
|
-
# provider-offer.sh so the Bun route prints the identical bytes (doctor
|
|
14167
|
-
# stdout is byte-compared -- tests/test-doctor-blocker-parity.sh). Skipped
|
|
14168
|
-
# silently, with no error, when loader.sh is missing or unsourceable.
|
|
14169
|
-
if declare -f render_provider_availability >/dev/null 2>&1; then
|
|
14170
|
-
if render_provider_availability; then
|
|
14171
|
-
echo ""
|
|
14172
|
-
fi
|
|
14173
|
-
fi
|
|
14174
|
-
|
|
14175
|
-
echo -e "${CYAN}API Keys:${NC}"
|
|
14176
|
-
# Note: CLI tools use their own login sessions outside Docker/K8s.
|
|
14177
|
-
# This section helps first-time users verify they can authenticate.
|
|
14178
|
-
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
|
14179
|
-
echo -e " ${GREEN}PASS${NC} ANTHROPIC_API_KEY is set"
|
|
14180
|
-
pass_count=$((pass_count + 1))
|
|
14181
|
-
elif command -v claude &>/dev/null; then
|
|
14182
|
-
# Zero-network login check. v7.104.2: prefer the CLI's own local
|
|
14183
|
-
# auth-status (JSON with "loggedIn"), which is authoritative for BOTH the
|
|
14184
|
-
# file-based and the native/Keychain login. This fixes doctor missing a
|
|
14185
|
-
# genuine Keychain login (native install writes NO ~/.claude/.credentials
|
|
14186
|
-
# .json). Fall back to the expiry stamp in the file for older CLIs.
|
|
14187
|
-
_claude_login="$(claude auth status 2>/dev/null | python3 -c "
|
|
14188
|
-
import json, sys
|
|
14189
|
-
try:
|
|
14190
|
-
v = json.load(sys.stdin).get('loggedIn')
|
|
14191
|
-
# Only the explicit booleans are trusted; a missing/renamed field is
|
|
14192
|
-
# inconclusive and falls through to the file-expiry check (never a spurious
|
|
14193
|
-
# 'not logged in' warning). Matches the run.sh helper + bun doctor.
|
|
14194
|
-
if v is True:
|
|
14195
|
-
print('yes')
|
|
14196
|
-
elif v is False:
|
|
14197
|
-
print('no')
|
|
14198
|
-
except Exception:
|
|
14199
|
-
pass
|
|
14200
|
-
" 2>/dev/null)"
|
|
14201
|
-
_claude_creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
14202
|
-
_claude_expired="$( [ -s "$_claude_creds" ] && python3 -c "
|
|
14203
|
-
import json, sys, time
|
|
14204
|
-
try:
|
|
14205
|
-
d = json.load(open(sys.argv[1]))
|
|
14206
|
-
exp = d.get('claudeAiOauth', {}).get('expiresAt')
|
|
14207
|
-
print('expired' if isinstance(exp,(int,float)) and exp>0 and (exp/1000.0)<=(time.time()+60) else 'ok')
|
|
14208
|
-
except Exception:
|
|
14209
|
-
print('ok')
|
|
14210
|
-
" "$_claude_creds" 2>/dev/null || echo ok )"
|
|
14211
|
-
if [ "$_claude_login" = "yes" ]; then
|
|
14212
|
-
echo -e " ${GREEN}PASS${NC} Claude CLI is logged in (subscription/OAuth login)"
|
|
14213
|
-
pass_count=$((pass_count + 1))
|
|
14214
|
-
elif [ "$_claude_login" = "no" ]; then
|
|
14215
|
-
# BLOCKER, not a warning. A build cannot run without a login, and the
|
|
14216
|
-
# warning form let a user pass doctor, answer every quickstart prompt,
|
|
14217
|
-
# pick a template and CONFIRM THE SPEND before hitting the refusal at
|
|
14218
|
-
# run.sh's auth preflight. Discovering a missing login after the
|
|
14219
|
-
# consent screen is the worst placement available. Doctor is where a
|
|
14220
|
-
# missing credential belongs. (An ANTHROPIC_API_KEY short-circuits
|
|
14221
|
-
# that preflight, which is why this branch is reached only when the
|
|
14222
|
-
# key is absent too.)
|
|
14223
|
-
echo -e " ${RED}FAIL${NC} Claude CLI is NOT logged in -- a build would stall instead of running"
|
|
14224
|
-
echo -e " ${YELLOW}Fix: claude login${NC} (or set ANTHROPIC_API_KEY)"
|
|
14225
|
-
_doctor_block "Claude CLI is not logged in. Fix: claude login (or set ANTHROPIC_API_KEY)"
|
|
14226
|
-
fail_count=$((fail_count + 1))
|
|
14227
|
-
elif [ "$_claude_expired" = "expired" ]; then
|
|
14228
|
-
echo -e " ${RED}FAIL${NC} Claude login has EXPIRED -- a build would stall instead of running"
|
|
14229
|
-
echo -e " ${YELLOW}Fix: claude login${NC} (or set ANTHROPIC_API_KEY)"
|
|
14230
|
-
_doctor_block "Claude login has expired. Fix: claude login (or set ANTHROPIC_API_KEY)"
|
|
14231
|
-
fail_count=$((fail_count + 1))
|
|
14232
|
-
else
|
|
14233
|
-
echo -e " ${DIM} -- ${NC} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)"
|
|
14234
|
-
fi
|
|
14235
|
-
fi
|
|
14236
|
-
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
14237
|
-
echo -e " ${GREEN}PASS${NC} OPENAI_API_KEY is set"
|
|
14238
|
-
pass_count=$((pass_count + 1))
|
|
14239
|
-
elif command -v codex &>/dev/null; then
|
|
14240
|
-
echo -e " ${DIM} -- ${NC} OPENAI_API_KEY not set (Codex CLI uses its own login)"
|
|
14241
|
-
fi
|
|
14242
|
-
# Phase I (v7.5.25): detect ANTHROPIC_BASE_URL alt-provider routing
|
|
14243
|
-
# (OpenRouter, Ollama, LiteLLM, self-hosted). Claude Code reads this env
|
|
14244
|
-
# var natively; Loki passes it through unchanged. Warn when the user sets
|
|
14245
|
-
# an alt endpoint without LOKI_MODEL_OVERRIDE -- the default opus/sonnet/
|
|
14246
|
-
# haiku aliases may not resolve on the alt-provider.
|
|
14247
|
-
if [ -n "${ANTHROPIC_BASE_URL:-}" ]; then
|
|
14248
|
-
echo -e " ${GREEN}PASS${NC} ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL}"
|
|
14249
|
-
pass_count=$((pass_count + 1))
|
|
14250
|
-
if [ -z "${LOKI_MODEL_OVERRIDE:-}" ]; then
|
|
14251
|
-
echo -e " ${YELLOW}WARN${NC} LOKI_MODEL_OVERRIDE not set -- opus/sonnet/haiku aliases may not resolve on alt-provider"
|
|
14252
|
-
warn_count=$((warn_count + 1))
|
|
13974
|
+
done
|
|
13975
|
+
if [ -z "$bun_bin" ]; then
|
|
13976
|
+
if [ "$found" = "1" ]; then
|
|
13977
|
+
_LOKI_DELEGATE_WHY="bun failed to start (found but 'bun --version' exited non-zero)"
|
|
13978
|
+
_LOKI_DELEGATE_FIX="Reinstall bun: curl -fsSL https://bun.sh/install | bash"
|
|
14253
13979
|
else
|
|
14254
|
-
|
|
14255
|
-
|
|
13980
|
+
_LOKI_DELEGATE_WHY="bun missing (not on PATH and no @oven/bun-* package under ${root})"
|
|
13981
|
+
_LOKI_DELEGATE_FIX="Install bun: curl -fsSL https://bun.sh/install | bash (or npm install -g bun)"
|
|
14256
13982
|
fi
|
|
13983
|
+
return 1
|
|
14257
13984
|
fi
|
|
14258
|
-
echo ""
|
|
14259
13985
|
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
"$HOME/.aider/skills/loki-mode:Aider CLI"
|
|
14266
|
-
)
|
|
14267
|
-
# SEVERITY DEPENDS ON WHOSE SKILL IT IS (v9.48.x). A dangling skill symlink
|
|
14268
|
-
# used to block unconditionally, so a stale link left by an old checkout
|
|
14269
|
-
# path exited 1 on a HEALTHY machine -- while the Provider Availability
|
|
14270
|
-
# section three lines above said that same provider was "not installed".
|
|
14271
|
-
# Doctor contradicted itself on one screen and told the user to fix a tool
|
|
14272
|
-
# they do not have.
|
|
14273
|
-
#
|
|
14274
|
-
# The link only blocks when it belongs to the provider a build would
|
|
14275
|
-
# actually launch. Anything else is inert state: still reported, with the
|
|
14276
|
-
# dangling target and the fix, but not a reason to refuse to start.
|
|
14277
|
-
#
|
|
14278
|
-
# `|| true` is mandatory under `set -euo pipefail`: the accessor returns
|
|
14279
|
-
# non-zero when no provider resolves (a bare call would abort doctor), and
|
|
14280
|
-
# an empty id then makes every entry non-blocking -- the same direction the
|
|
14281
|
-
# No-AI-provider blocker above already covers.
|
|
14282
|
-
local _selected_provider=""
|
|
14283
|
-
if declare -f effective_provider_id >/dev/null 2>&1; then
|
|
14284
|
-
_selected_provider="$(effective_provider_id 2>/dev/null || true)"
|
|
14285
|
-
fi
|
|
14286
|
-
for entry in "${skill_dirs[@]}"; do
|
|
14287
|
-
local sdir="${entry%%:*}"
|
|
14288
|
-
local sname="${entry#*:}"
|
|
14289
|
-
local short_path="${sdir/$HOME/~}"
|
|
14290
|
-
# Provider id from the directory, never a parallel list: `.claude` ->
|
|
14291
|
-
# `claude`. A fifth hand-maintained list is how the lists in this repo
|
|
14292
|
-
# have drifted from auto_detect_provider before (v8.76.0, v8.82.0).
|
|
14293
|
-
local _sid="${sdir#"$HOME"/.}"
|
|
14294
|
-
_sid="${_sid%%/*}"
|
|
14295
|
-
if [ -f "$sdir/SKILL.md" ]; then
|
|
14296
|
-
echo -e " ${GREEN}PASS${NC} $sname ${DIM}$short_path${NC}"
|
|
14297
|
-
pass_count=$((pass_count + 1))
|
|
14298
|
-
elif [ -L "$sdir" ]; then
|
|
14299
|
-
local _target
|
|
14300
|
-
# readlink is NOT in POSIX and is absent from minimal images and
|
|
14301
|
-
# from constrained PATHs (the doctor parity harness builds one).
|
|
14302
|
-
# When it is missing this reported the literal string "unknown"
|
|
14303
|
-
# while the Bun route printed the real target, so the two routes
|
|
14304
|
-
# diverged on the same host and bun-parity went red. ls -ld is
|
|
14305
|
-
# POSIX and always present; strip through the FIRST " -> " only,
|
|
14306
|
-
# since a link target may itself contain that sequence.
|
|
14307
|
-
if command -v readlink >/dev/null 2>&1; then
|
|
14308
|
-
_target=$(readlink "$sdir" 2>/dev/null || echo "unknown")
|
|
14309
|
-
else
|
|
14310
|
-
_target=$(ls -ld "$sdir" 2>/dev/null | sed -e 's/^[^>]*-> //')
|
|
14311
|
-
[ -n "$_target" ] || _target="unknown"
|
|
14312
|
-
fi
|
|
14313
|
-
# Blocking ONLY for the provider a build would actually use. The
|
|
14314
|
-
# non-blocking arm is a WARN rather than a non-counted FAIL on
|
|
14315
|
-
# purpose: a red FAIL badge that never reaches `N failed` or the
|
|
14316
|
-
# `Blocking (N)` list makes the summary disagree with the screen.
|
|
14317
|
-
# Either way the dangling target and the fix are still printed --
|
|
14318
|
-
# the stale link is reported, never silently swallowed.
|
|
14319
|
-
if [ -n "$_selected_provider" ] && [ "$_sid" = "$_selected_provider" ]; then
|
|
14320
|
-
echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
|
|
14321
|
-
echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
|
|
14322
|
-
_doctor_block "$sname is a broken symlink. Fix: loki setup-skill"
|
|
14323
|
-
fail_count=$((fail_count + 1))
|
|
14324
|
-
else
|
|
14325
|
-
echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
|
|
14326
|
-
echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
|
|
14327
|
-
warn_count=$((warn_count + 1))
|
|
14328
|
-
fi
|
|
14329
|
-
else
|
|
14330
|
-
echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(not found - run 'loki setup-skill')${NC}"
|
|
14331
|
-
warn_count=$((warn_count + 1))
|
|
14332
|
-
fi
|
|
14333
|
-
done
|
|
14334
|
-
echo ""
|
|
14335
|
-
|
|
14336
|
-
echo -e "${CYAN}Integrations:${NC}"
|
|
14337
|
-
# MCP SDK check
|
|
14338
|
-
if python3 -c "import mcp" 2>/dev/null; then
|
|
14339
|
-
echo -e " ${GREEN}PASS${NC} MCP SDK (Python)"
|
|
14340
|
-
pass_count=$((pass_count + 1))
|
|
14341
|
-
else
|
|
14342
|
-
echo -e " ${YELLOW}WARN${NC} MCP SDK - not installed (pip3 install mcp)"
|
|
14343
|
-
warn_count=$((warn_count + 1))
|
|
14344
|
-
fi
|
|
14345
|
-
# Detect best Python for ML packages (3.12 preferred, 3.14+ has compat issues)
|
|
14346
|
-
local _py_ml="python3"
|
|
14347
|
-
if command -v /opt/homebrew/bin/python3.12 &>/dev/null; then
|
|
14348
|
-
_py_ml="/opt/homebrew/bin/python3.12"
|
|
14349
|
-
elif command -v python3.12 &>/dev/null; then
|
|
14350
|
-
_py_ml="python3.12"
|
|
14351
|
-
fi
|
|
14352
|
-
# Vector search dependencies
|
|
14353
|
-
if $_py_ml -c "import numpy" 2>/dev/null; then
|
|
14354
|
-
echo -e " ${GREEN}PASS${NC} numpy (vector search)"
|
|
14355
|
-
pass_count=$((pass_count + 1))
|
|
14356
|
-
else
|
|
14357
|
-
echo -e " ${YELLOW}WARN${NC} numpy - not installed (pip3 install numpy)"
|
|
14358
|
-
warn_count=$((warn_count + 1))
|
|
14359
|
-
fi
|
|
14360
|
-
if $_py_ml -c "import sentence_transformers" 2>/dev/null; then
|
|
14361
|
-
echo -e " ${GREEN}PASS${NC} sentence-transformers (embeddings)"
|
|
14362
|
-
pass_count=$((pass_count + 1))
|
|
14363
|
-
else
|
|
14364
|
-
echo -e " ${YELLOW}WARN${NC} sentence-transformers - not installed (loki memory vectors setup)"
|
|
14365
|
-
warn_count=$((warn_count + 1))
|
|
14366
|
-
fi
|
|
14367
|
-
# C3 (doctor UX): the next checks are bounded network probes, not instant
|
|
14368
|
-
# local lookups. They are the slowest part of doctor (a stale/unreachable
|
|
14369
|
-
# host could otherwise hang on curl's default ~2min connect timeout). We
|
|
14370
|
-
# (a) bound every probe with --connect-timeout/--max-time so a dead host
|
|
14371
|
-
# fails fast, and (b) print a transient progress note to stderr on a TTY so
|
|
14372
|
-
# the user knows doctor is doing network work, not stuck. The note goes to
|
|
14373
|
-
# stderr only -- the parity-captured stdout stays byte-identical across the
|
|
14374
|
-
# bash and Bun routes.
|
|
14375
|
-
doctor_probe_note() {
|
|
14376
|
-
# return 0 is mandatory: [ -t 2 ] returns 1 off a TTY, and under
|
|
14377
|
-
# set -euo pipefail a bare call here aborts the whole doctor run on
|
|
14378
|
-
# CI/headless (the v7.58.1 Bun-Parity crash).
|
|
14379
|
-
[ -t 2 ] && printf ' %s...\r' "$1" >&2
|
|
14380
|
-
return 0
|
|
14381
|
-
}
|
|
14382
|
-
doctor_probe_clear() {
|
|
14383
|
-
# Erase the transient progress line so it does not linger above the
|
|
14384
|
-
# PASS/WARN result. Only on a TTY (where the note was printed).
|
|
14385
|
-
# return 0: same set -e safety as doctor_probe_note.
|
|
14386
|
-
[ -t 2 ] && printf '\r\033[K' >&2
|
|
14387
|
-
return 0
|
|
14388
|
-
}
|
|
14389
|
-
# ChromaDB check (bounded network probe)
|
|
14390
|
-
doctor_probe_note "Probing ChromaDB (port 8100)"
|
|
14391
|
-
if curl -sf --connect-timeout 2 --max-time 5 http://localhost:8100/api/v2/heartbeat >/dev/null 2>&1; then
|
|
14392
|
-
doctor_probe_clear
|
|
14393
|
-
echo -e " ${GREEN}PASS${NC} ChromaDB server (port 8100)"
|
|
14394
|
-
pass_count=$((pass_count + 1))
|
|
14395
|
-
else
|
|
14396
|
-
doctor_probe_clear
|
|
14397
|
-
echo -e " ${YELLOW}WARN${NC} ChromaDB - not running (docker start loki-chroma)"
|
|
14398
|
-
warn_count=$((warn_count + 1))
|
|
14399
|
-
fi
|
|
14400
|
-
# LSP servers check (v7.7.0): auto-detected by mcp.lsp_proxy; reported
|
|
14401
|
-
# here so users know which languages get agent-side symbol grounding.
|
|
14402
|
-
local _lsp_found=0 _lsp_bin
|
|
14403
|
-
local _lsp_list=""
|
|
14404
|
-
for _lsp_bin in pyright-langserver pylsp typescript-language-server gopls rust-analyzer jdtls; do
|
|
14405
|
-
if command -v "$_lsp_bin" >/dev/null 2>&1; then
|
|
14406
|
-
_lsp_found=$((_lsp_found + 1))
|
|
14407
|
-
_lsp_list="${_lsp_list:+$_lsp_list, }${_lsp_bin}"
|
|
13986
|
+
if [ -n "${LOKI_TS_ENTRY:-}" ]; then
|
|
13987
|
+
if [ ! -f "$LOKI_TS_ENTRY" ]; then
|
|
13988
|
+
_LOKI_DELEGATE_WHY="LOKI_TS_ENTRY invalid (${LOKI_TS_ENTRY} does not exist)"
|
|
13989
|
+
_LOKI_DELEGATE_FIX="Unset LOKI_TS_ENTRY or point it at a built loki.js"
|
|
13990
|
+
return 1
|
|
14408
13991
|
fi
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
warn_count=$((warn_count + 1))
|
|
14416
|
-
fi
|
|
14417
|
-
# MiroFish check (optional service)
|
|
14418
|
-
local _mf_url="${LOKI_MIROFISH_URL:-http://localhost:5001}"
|
|
14419
|
-
if docker inspect loki-mirofish &>/dev/null 2>&1 || [[ -n "${LOKI_MIROFISH_URL:-}" ]]; then
|
|
14420
|
-
doctor_probe_note "Probing MiroFish ($_mf_url)"
|
|
14421
|
-
if curl -sf --connect-timeout 2 --max-time 5 "$_mf_url/health" >/dev/null 2>&1; then
|
|
14422
|
-
doctor_probe_clear
|
|
14423
|
-
echo -e " ${GREEN}PASS${NC} MiroFish server ($_mf_url)"
|
|
14424
|
-
pass_count=$((pass_count + 1))
|
|
14425
|
-
else
|
|
14426
|
-
doctor_probe_clear
|
|
14427
|
-
echo -e " ${YELLOW}WARN${NC} MiroFish - not running (loki start --mirofish-docker <image>)"
|
|
14428
|
-
warn_count=$((warn_count + 1))
|
|
13992
|
+
cli="$LOKI_TS_ENTRY"
|
|
13993
|
+
elif [ -f "$root/loki-ts/dist/loki.js" ]; then
|
|
13994
|
+
cli="$root/loki-ts/dist/loki.js"
|
|
13995
|
+
# Dev checkout: a gitignored dist older than src is stale (same rule as bin/loki).
|
|
13996
|
+
if [ -f "$root/loki-ts/src/cli.ts" ] && [ "$root/loki-ts/src/cli.ts" -nt "$cli" ]; then
|
|
13997
|
+
cli="$root/loki-ts/src/cli.ts"
|
|
14429
13998
|
fi
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
if [ -n "${LOKI_OTEL_ENDPOINT:-}" ]; then
|
|
14433
|
-
echo -e " ${GREEN}PASS${NC} OTEL endpoint: $LOKI_OTEL_ENDPOINT"
|
|
14434
|
-
pass_count=$((pass_count + 1))
|
|
13999
|
+
elif [ -f "$root/loki-ts/src/cli.ts" ]; then
|
|
14000
|
+
cli="$root/loki-ts/src/cli.ts"
|
|
14435
14001
|
else
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
# sentrux check (v7.5.14, optional architectural-drift gate)
|
|
14440
|
-
if command -v sentrux &>/dev/null; then
|
|
14441
|
-
local _sentrux_ver
|
|
14442
|
-
_sentrux_ver=$(sentrux --version 2>/dev/null | head -1 | awk '{print $NF}')
|
|
14443
|
-
echo -e " ${GREEN}PASS${NC} sentrux ${_sentrux_ver:-unknown} (architectural drift gate: loki sentrux help)"
|
|
14444
|
-
pass_count=$((pass_count + 1))
|
|
14445
|
-
else
|
|
14446
|
-
echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
|
|
14447
|
-
warn_count=$((warn_count + 1))
|
|
14448
|
-
fi
|
|
14449
|
-
# Receipt signing: Ed25519, auto-generated on first run. WARN only, and
|
|
14450
|
-
# only when something needs attention. Byte-mirrored in doctor.ts.
|
|
14451
|
-
if [ -n "${LOKI_PROOF_GPG_KEY:-}" ]; then
|
|
14452
|
-
echo -e " ${YELLOW}WARN${NC} Receipt signing: LOKI_PROOF_GPG_KEY is no longer supported and is ignored; use LOKI_RECEIPT_SIGNING_KEY or LOKI_RECEIPT_SIGNING_KEY_FILE (docs/SIGNED-RECEIPTS.md)"
|
|
14453
|
-
warn_count=$((warn_count + 1))
|
|
14454
|
-
fi
|
|
14455
|
-
if ! python3 -c 'import cryptography' >/dev/null 2>&1; then
|
|
14456
|
-
echo -e " ${YELLOW}WARN${NC} Receipt signing: python3 'cryptography' package missing, receipts will be UNSIGNED (pip install cryptography)"
|
|
14457
|
-
warn_count=$((warn_count + 1))
|
|
14458
|
-
fi
|
|
14459
|
-
echo ""
|
|
14460
|
-
|
|
14461
|
-
echo -e "${CYAN}System:${NC}"
|
|
14462
|
-
doctor_check "bash (>= 4.0)" bash recommended 4.0 || true
|
|
14463
|
-
# v7.4.9: Bun powers ported commands; warn if missing so users notice the
|
|
14464
|
-
# speedup is unavailable (bin/loki falls through to bash silently).
|
|
14465
|
-
doctor_check "Bun (>= 1.3)" bun recommended 1.3 || true
|
|
14466
|
-
|
|
14467
|
-
# Disk space check
|
|
14468
|
-
local disk_avail
|
|
14469
|
-
if command -v df &> /dev/null; then
|
|
14470
|
-
# Get available space in GB (works on macOS and Linux).
|
|
14471
|
-
# v7.4.12: || true on the pipes so set -euo pipefail doesn't exit
|
|
14472
|
-
# the whole doctor when df -g fails on Linux (which doesn't support
|
|
14473
|
-
# the -g flag). Pre-v7.4.12 bash route truncated after the System
|
|
14474
|
-
# section on Linux because of this exact early exit.
|
|
14475
|
-
disk_avail=$(df -g "$HOME" 2>/dev/null | tail -1 | awk '{print $4}' || true)
|
|
14476
|
-
if [ -z "$disk_avail" ]; then
|
|
14477
|
-
# Linux fallback (df -g may not work)
|
|
14478
|
-
disk_avail=$(df -BG "$HOME" 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G' || true)
|
|
14479
|
-
fi
|
|
14480
|
-
if [ -n "$disk_avail" ] && [ "$disk_avail" -gt 0 ] 2>/dev/null; then
|
|
14481
|
-
if [ "$disk_avail" -lt 1 ]; then
|
|
14482
|
-
echo -e " ${RED}FAIL${NC} Disk space: ${disk_avail}GB available (need >= 1GB)"
|
|
14483
|
-
_doctor_block "Free up disk: ${disk_avail}GB available, need >= 1GB"
|
|
14484
|
-
fail_count=$((fail_count + 1))
|
|
14485
|
-
elif [ "$disk_avail" -lt 5 ]; then
|
|
14486
|
-
echo -e " ${YELLOW}WARN${NC} Disk space: ${disk_avail}GB available (low)"
|
|
14487
|
-
warn_count=$((warn_count + 1))
|
|
14488
|
-
else
|
|
14489
|
-
echo -e " ${GREEN}PASS${NC} Disk space: ${disk_avail}GB available"
|
|
14490
|
-
pass_count=$((pass_count + 1))
|
|
14491
|
-
fi
|
|
14492
|
-
else
|
|
14493
|
-
echo -e " ${YELLOW}WARN${NC} Disk space: unable to determine"
|
|
14494
|
-
warn_count=$((warn_count + 1))
|
|
14495
|
-
fi
|
|
14002
|
+
_LOKI_DELEGATE_WHY="dist missing (no loki-ts/dist/loki.js or loki-ts/src/cli.ts under ${root})"
|
|
14003
|
+
_LOKI_DELEGATE_FIX="Reinstall loki-mode (npm install -g loki-mode) or run: cd loki-ts && bun run build"
|
|
14004
|
+
return 1
|
|
14496
14005
|
fi
|
|
14497
|
-
echo ""
|
|
14498
14006
|
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
# totals across routes.
|
|
14503
|
-
echo -e "${CYAN}Runtime route:${NC}"
|
|
14504
|
-
echo -e " ${GREEN}PASS${NC} Active runtime: Bash (autonomy/loki)"
|
|
14505
|
-
if [ "${LOKI_LEGACY_BASH:-}" = "1" ] || [ "${LOKI_LEGACY_BASH:-}" = "true" ]; then
|
|
14506
|
-
echo -e " ${YELLOW}WARN${NC} LOKI_LEGACY_BASH set: shim routes every command to autonomy/loki (bash)"
|
|
14507
|
-
fi
|
|
14508
|
-
if [ -n "${LOKI_TS_ENTRY:-}" ]; then
|
|
14509
|
-
echo -e " ${GREEN}PASS${NC} LOKI_TS_ENTRY override: ${LOKI_TS_ENTRY}"
|
|
14510
|
-
fi
|
|
14511
|
-
if [ "${BUN_FROM_SOURCE:-}" = "1" ] || [ "${BUN_FROM_SOURCE:-}" = "true" ]; then
|
|
14512
|
-
echo -e " ${GREEN}PASS${NC} BUN_FROM_SOURCE set: shim prefers loki-ts/src/ over dist/"
|
|
14513
|
-
fi
|
|
14514
|
-
# v7.5.2: explicit python3.12 probe -- chromadb + sentence-transformers
|
|
14515
|
-
# require it; the generic Python 3 check passes Python 3.13/3.14 and
|
|
14516
|
-
# silently fails at runtime. Informational only.
|
|
14517
|
-
py312_path=$(command -v python3.12 2>/dev/null || true)
|
|
14518
|
-
if [ -n "$py312_path" ]; then
|
|
14519
|
-
py312_ver=$("$py312_path" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")' 2>/dev/null)
|
|
14520
|
-
echo -e " ${GREEN}PASS${NC} Python 3.12 (chromadb / sentence-transformers): ${py312_ver} at ${py312_path}"
|
|
14007
|
+
t="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
|
|
14008
|
+
if [ -n "$t" ]; then
|
|
14009
|
+
"$t" -k 1 5 "$bun_bin" "$cli" version >/dev/null 2>&1
|
|
14521
14010
|
else
|
|
14522
|
-
|
|
14011
|
+
"$bun_bin" "$cli" version >/dev/null 2>&1
|
|
14523
14012
|
fi
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
# I/O, so `loki doctor` stays air-gapped-safe (docs/air-gapped.md). We do not
|
|
14530
|
-
# and will not auto-fetch or guess model IDs; inventing a model that does not
|
|
14531
|
-
# exist is worse than being stale, so refresh stays a human-verified step.
|
|
14532
|
-
#
|
|
14533
|
-
# Informational only -- like Runtime route and Cockpit above, it does NOT
|
|
14534
|
-
# touch pass/warn/fail counts and never calls _doctor_block, so a stale
|
|
14535
|
-
# catalog can never flip doctor's exit code or block a build. Advisory by
|
|
14536
|
-
# construction, not by convention.
|
|
14537
|
-
#
|
|
14538
|
-
# Mirrored byte-for-byte in loki-ts/src/commands/doctor.ts -- the bun-parity
|
|
14539
|
-
# matrix diffs this section (it is NOT in the normalizer's strip list), so
|
|
14540
|
-
# edit BOTH routes or parity fails.
|
|
14541
|
-
echo -e "${CYAN}Model catalog:${NC}"
|
|
14542
|
-
_catalog_path="${LOKI_MODEL_CATALOG:-${_LOKI_SCRIPT_DIR}/../providers/model_catalog.json}"
|
|
14543
|
-
_catalog_line=$(LOKI_CATALOG_PATH="$_catalog_path" python3 -c "
|
|
14544
|
-
import json, os, datetime
|
|
14545
|
-
# Threshold: 90 days. The upstream probe workflow runs weekly, so a catalog
|
|
14546
|
-
# untouched for a quarter means the probe PRs are going unread.
|
|
14547
|
-
STALE_DAYS = 90
|
|
14548
|
-
p = os.environ['LOKI_CATALOG_PATH']
|
|
14549
|
-
try:
|
|
14550
|
-
updated = json.load(open(p))['updated']
|
|
14551
|
-
age = (datetime.datetime.now(datetime.timezone.utc).date() - datetime.date.fromisoformat(updated)).days
|
|
14552
|
-
except Exception:
|
|
14553
|
-
print('warn|Catalog unreadable or missing an ISO \"updated\" date -- cannot determine age')
|
|
14554
|
-
else:
|
|
14555
|
-
if age > STALE_DAYS:
|
|
14556
|
-
print(f'warn|Last updated {updated} ({age} days ago) -- may be missing newer models. Refresh: python3 tools/probe-model-catalog.py (reports new model IDs from provider docs; you verify and edit the catalog by hand -- never auto-applied)')
|
|
14557
|
-
else:
|
|
14558
|
-
print(f'pass|Last updated {updated} ({age} days ago)')
|
|
14559
|
-
" 2>/dev/null)
|
|
14560
|
-
# Fail-open: if python3 is missing or the probe dies, say so and move on.
|
|
14561
|
-
[ -n "$_catalog_line" ] || _catalog_line="warn|Could not probe catalog age"
|
|
14562
|
-
if [ "${_catalog_line%%|*}" = "pass" ]; then
|
|
14563
|
-
echo -e " ${GREEN}PASS${NC} ${_catalog_line#*|}"
|
|
14564
|
-
else
|
|
14565
|
-
echo -e " ${YELLOW}WARN${NC} ${_catalog_line#*|}"
|
|
14013
|
+
rc=$?
|
|
14014
|
+
if [ "$rc" -ne 0 ]; then
|
|
14015
|
+
_LOKI_DELEGATE_WHY="bun failed exit ${rc} running ${cli} version"
|
|
14016
|
+
_LOKI_DELEGATE_FIX="Reinstall loki-mode (npm install -g loki-mode) or unset LOKI_TS_ENTRY"
|
|
14017
|
+
return 1
|
|
14566
14018
|
fi
|
|
14567
|
-
echo ""
|
|
14568
14019
|
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
# like Runtime route above), so the bun-parity matrix stays reconcilable.
|
|
14572
|
-
# The protocol + resvg + path come from a bun probe that reuses the real
|
|
14573
|
-
# detection code (loki-ts) so this report can never drift from behavior;
|
|
14574
|
-
# with no bun we honestly report the text-only path.
|
|
14575
|
-
echo -e "${CYAN}Cockpit:${NC}"
|
|
14576
|
-
# Truecolor: purely from COLORTERM, same signal the renderer relies on.
|
|
14577
|
-
case "${COLORTERM:-}" in
|
|
14578
|
-
truecolor|24bit)
|
|
14579
|
-
echo -e " ${GREEN}PASS${NC} Truecolor: yes (COLORTERM=${COLORTERM})" ;;
|
|
14580
|
-
*)
|
|
14581
|
-
echo -e " ${DIM} -- ${NC} Truecolor: not signalled (COLORTERM unset) -- colors may be approximate" ;;
|
|
14582
|
-
esac
|
|
14583
|
-
local _ck_ts_dir="${_LOKI_SCRIPT_DIR}/../loki-ts"
|
|
14584
|
-
local _ck_entry="$_ck_ts_dir/dist/cockpit.js"
|
|
14585
|
-
[ -f "$_ck_entry" ] || _ck_entry="$_ck_ts_dir/src/cockpit/cli.ts"
|
|
14586
|
-
if command -v bun >/dev/null 2>&1 && [ -f "$_ck_entry" ]; then
|
|
14587
|
-
echo -e " ${GREEN}PASS${NC} Bun present -- cockpit renderer available"
|
|
14588
|
-
local _ck_probe _ck_proto _ck_resvg _ck_path
|
|
14589
|
-
_ck_probe="$(bun "$_ck_entry" --probe 2>/dev/null || true)"
|
|
14590
|
-
_ck_proto="$(printf '%s\n' "$_ck_probe" | sed -n 's/^protocol=//p' | head -1)"
|
|
14591
|
-
_ck_resvg="$(printf '%s\n' "$_ck_probe" | sed -n 's/^resvg=//p' | head -1)"
|
|
14592
|
-
_ck_path="$(printf '%s\n' "$_ck_probe" | sed -n 's/^path=//p' | head -1)"
|
|
14593
|
-
[ -n "$_ck_proto" ] || _ck_proto="none"
|
|
14594
|
-
[ -n "$_ck_resvg" ] || _ck_resvg="no"
|
|
14595
|
-
[ -n "$_ck_path" ] || _ck_path="text+dashboard"
|
|
14596
|
-
if [ "$_ck_proto" = "none" ]; then
|
|
14597
|
-
echo -e " ${DIM} -- ${NC} Inline-image protocol: none (this terminal has no iTerm2/Kitty graphics)"
|
|
14598
|
-
else
|
|
14599
|
-
echo -e " ${GREEN}PASS${NC} Inline-image protocol: ${_ck_proto}"
|
|
14600
|
-
fi
|
|
14601
|
-
if [ "$_ck_resvg" = "yes" ]; then
|
|
14602
|
-
echo -e " ${GREEN}PASS${NC} SVG rasterizer: ready (bundled wasm; renders the frame to an image)"
|
|
14603
|
-
else
|
|
14604
|
-
echo -e " ${DIM} -- ${NC} SVG rasterizer: unavailable (cockpit uses text+dashboard)"
|
|
14605
|
-
fi
|
|
14606
|
-
if [ "$_ck_path" = "image" ]; then
|
|
14607
|
-
echo -e " ${GREEN}PASS${NC} Render path: inline image ('loki cockpit' draws the frame in-terminal)"
|
|
14608
|
-
else
|
|
14609
|
-
echo -e " ${DIM} -- ${NC} Render path: text + browser dashboard ('loki cockpit' prints a summary; open 'loki dashboard')"
|
|
14610
|
-
fi
|
|
14611
|
-
else
|
|
14612
|
-
echo -e " ${YELLOW}WARN${NC} Bun not found -- 'loki cockpit' uses the text summary + browser dashboard (install bun for the inline-image frame)"
|
|
14613
|
-
# Report the protocol line here too. Without it doctor's Cockpit section
|
|
14614
|
-
# is asymmetric: the bun branch prints BOTH "Inline-image protocol" and
|
|
14615
|
-
# "Render path", the no-bun branch printed only "Render path". A user
|
|
14616
|
-
# reading doctor on a machine without bun could not tell whether the
|
|
14617
|
-
# inline-image path was unsupported by their terminal or simply never
|
|
14618
|
-
# probed. Naming the reason keeps the section self-explanatory.
|
|
14619
|
-
echo -e " ${DIM} -- ${NC} Inline-image protocol: not probed (needs bun to run the cockpit renderer)"
|
|
14620
|
-
echo -e " ${DIM} -- ${NC} Render path: text + browser dashboard"
|
|
14621
|
-
fi
|
|
14622
|
-
echo ""
|
|
14020
|
+
exec "$bun_bin" "$cli" "$cmd" "$@"
|
|
14021
|
+
}
|
|
14623
14022
|
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
echo -e "${BOLD}Install integrity:${NC}"
|
|
14637
|
-
local _det_missing="" _det
|
|
14638
|
-
for _det in detect-test-mutations detect-mock-problems \
|
|
14639
|
-
detect-semantic-test-problems detect-invariant-violations; do
|
|
14640
|
-
if [ -f "${_LOKI_SCRIPT_DIR}/../tests/${_det}.sh" ]; then
|
|
14641
|
-
pass_count=$((pass_count + 1))
|
|
14642
|
-
else
|
|
14643
|
-
_det_missing="${_det_missing} ${_det}.sh"
|
|
14644
|
-
fi
|
|
14023
|
+
# loki doctor. The one implementation is loki-ts/src/commands/doctor.ts; this is a
|
|
14024
|
+
# delegator. Only --airgap stays in bash (_loki_airgap_audit). If the Bun route is
|
|
14025
|
+
# unavailable we print a minimal diagnostic (never a second copy of the checks).
|
|
14026
|
+
cmd_doctor() {
|
|
14027
|
+
local json_output=false
|
|
14028
|
+
local airgap_audit=false
|
|
14029
|
+
local a
|
|
14030
|
+
for a in "$@"; do
|
|
14031
|
+
case "$a" in
|
|
14032
|
+
--json) json_output=true ;;
|
|
14033
|
+
--airgap) airgap_audit=true ;;
|
|
14034
|
+
esac
|
|
14645
14035
|
done
|
|
14646
|
-
if [ -z "$_det_missing" ]; then
|
|
14647
|
-
echo -e " ${GREEN}OK${NC} Quality-gate detectors present (4/4)"
|
|
14648
|
-
else
|
|
14649
|
-
echo -e " ${RED}FAIL${NC} Quality-gate detectors MISSING:${_det_missing}"
|
|
14650
|
-
echo -e " ${DIM} These gates fail-closed, so every iteration will be blocked.${NC}"
|
|
14651
|
-
_doctor_block "Incomplete install: quality-gate detectors are missing. Reinstall: bun install -g loki-mode"
|
|
14652
|
-
fail_count=$((fail_count + 1))
|
|
14653
|
-
fi
|
|
14654
|
-
echo ""
|
|
14655
14036
|
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
# PATH (typically ~/.local/bin -> a Homebrew node prefix, ahead of
|
|
14660
|
-
# ~/.bun/bin). Reinstalling cannot fix it; every reinstall updates the copy
|
|
14661
|
-
# that is NOT winning, so the user loops forever on advice that cannot work.
|
|
14662
|
-
#
|
|
14663
|
-
# This is a blocker, not a warning: the user is running code they did not
|
|
14664
|
-
# install and cannot upgrade by the documented route.
|
|
14665
|
-
#
|
|
14666
|
-
# Resolved by realpath so two PATH entries pointing at the SAME install are
|
|
14667
|
-
# not reported as shadowing each other.
|
|
14668
|
-
# Resolve symlinks with whatever the host has. macOS ships no `realpath`
|
|
14669
|
-
# on older versions, so fall back to readlink and finally to the path
|
|
14670
|
-
# itself -- an unresolved path is still comparable, just less precise.
|
|
14671
|
-
_doctor_resolve() {
|
|
14672
|
-
if command -v realpath >/dev/null 2>&1; then
|
|
14673
|
-
realpath "$1" 2>/dev/null || printf '%s' "$1"
|
|
14674
|
-
elif command -v readlink >/dev/null 2>&1; then
|
|
14675
|
-
readlink -f "$1" 2>/dev/null || printf '%s' "$1"
|
|
14676
|
-
else
|
|
14677
|
-
printf '%s' "$1"
|
|
14678
|
-
fi
|
|
14679
|
-
}
|
|
14680
|
-
_running_real="$(command -v loki 2>/dev/null || true)"
|
|
14681
|
-
if [ -n "$_running_real" ]; then
|
|
14682
|
-
_running_real="$(_doctor_resolve "$_running_real")"
|
|
14683
|
-
_shadow_found=""
|
|
14684
|
-
_shadow_seen=""
|
|
14685
|
-
_oldIFS="$IFS"; IFS=':'
|
|
14686
|
-
for _d in $PATH; do
|
|
14687
|
-
[ -n "$_d" ] || continue
|
|
14688
|
-
[ -x "$_d/loki" ] || continue
|
|
14689
|
-
_r="$(_doctor_resolve "$_d/loki")"
|
|
14690
|
-
case "$_shadow_seen" in *"|$_r|"*) continue ;; esac
|
|
14691
|
-
_shadow_seen="${_shadow_seen}|$_r|"
|
|
14692
|
-
[ "$_r" = "$_running_real" ] && continue
|
|
14693
|
-
# <pkg>/bin/loki -> package.json two levels up
|
|
14694
|
-
_pj="$(dirname "$(dirname "$_r")")/package.json"
|
|
14695
|
-
[ -f "$_pj" ] || continue
|
|
14696
|
-
_v="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1])).get('version',''))" "$_pj" 2>/dev/null || true)"
|
|
14697
|
-
[ -n "$_v" ] || continue
|
|
14698
|
-
_shadow_found="${_shadow_found}\n $_v at $_d/loki"
|
|
14699
|
-
done
|
|
14700
|
-
IFS="$_oldIFS"
|
|
14701
|
-
if [ -n "$_shadow_found" ]; then
|
|
14702
|
-
echo -e " ${RED}FAIL${NC} Multiple loki installs on PATH; you are running $(get_version 2>/dev/null || echo unknown)"
|
|
14703
|
-
echo -e " ${DIM} Running: $_running_real${NC}"
|
|
14704
|
-
printf " ${DIM} Others:%b${NC}\n" "$_shadow_found"
|
|
14705
|
-
echo -e " ${DIM} Reinstalling updates a copy that is not winning on PATH.${NC}"
|
|
14706
|
-
_doctor_block "Multiple loki installs on PATH. Run 'which -a loki', then remove or re-point every entry EARLIER than the one you want. Reinstalling alone will not fix this."
|
|
14707
|
-
fail_count=$((fail_count + 1))
|
|
14708
|
-
else
|
|
14709
|
-
echo -e " ${GREEN}OK${NC} Single loki install on PATH"
|
|
14710
|
-
fi
|
|
14711
|
-
echo ""
|
|
14037
|
+
if [ "$airgap_audit" = true ]; then
|
|
14038
|
+
_loki_airgap_audit "$json_output"
|
|
14039
|
+
return $?
|
|
14712
14040
|
fi
|
|
14713
14041
|
|
|
14714
|
-
#
|
|
14715
|
-
|
|
14716
|
-
|
|
14042
|
+
# LOKI_LEGACY_BASH stays set: doctor.ts reads it to report the bash runtime
|
|
14043
|
+
# and print the warning; it never re-routes on it.
|
|
14044
|
+
_loki_bun_delegate doctor "$@" || true
|
|
14717
14045
|
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
echo ""
|
|
14722
|
-
echo "Then re-run: loki doctor"
|
|
14723
|
-
echo "Meanwhile 'loki tour' works right now -- no provider, no key, no spend."
|
|
14724
|
-
# Adoption signal: which CLASS of dependency stopped this first run.
|
|
14725
|
-
# Mapped to an enum here rather than forwarding the blocker TEXT, so
|
|
14726
|
-
# the payload can never carry a path or a version. Best-effort and
|
|
14727
|
-
# backgrounded: telemetry must never delay or fail a doctor run, and
|
|
14728
|
-
# every existing opt-out still gates it downstream.
|
|
14729
|
-
if declare -f loki_emit_first_run_blocked >/dev/null 2>&1; then
|
|
14730
|
-
local _blk_key="other"
|
|
14731
|
-
case "$_doctor_blockers" in
|
|
14732
|
-
*"No AI provider CLI"*) _blk_key="no_provider" ;;
|
|
14733
|
-
*"Node.js is not installed"*|*"Node.js must be"*) _blk_key="node" ;;
|
|
14734
|
-
*"Python 3 is not installed"*|*"Python 3 must be"*) _blk_key="python3" ;;
|
|
14735
|
-
*"jq is not installed"*) _blk_key="jq" ;;
|
|
14736
|
-
*"git is not installed"*) _blk_key="git" ;;
|
|
14737
|
-
*"curl is not installed"*) _blk_key="curl" ;;
|
|
14738
|
-
*"Free up disk"*) _blk_key="disk" ;;
|
|
14739
|
-
*"broken symlink"*) _blk_key="skill_symlink" ;;
|
|
14740
|
-
esac
|
|
14741
|
-
( loki_emit_first_run_blocked "$_blk_key" >/dev/null 2>&1 </dev/null & ) 2>/dev/null || true
|
|
14742
|
-
fi
|
|
14743
|
-
# A-123: the LAST line is the one blocking reason, exactly.
|
|
14744
|
-
printf '%b' "$_doctor_blockers" | awk '/^ - /{sub(/^ - /,""); print; exit}'
|
|
14745
|
-
return 1
|
|
14746
|
-
elif [ "$warn_count" -gt 0 ]; then
|
|
14747
|
-
echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
|
|
14046
|
+
# Reached only when the delegate could not exec.
|
|
14047
|
+
if [ "$json_output" = true ]; then
|
|
14048
|
+
cmd_doctor_json
|
|
14748
14049
|
else
|
|
14749
|
-
echo
|
|
14050
|
+
echo "loki doctor: the Bun doctor is unavailable, so only this minimal report can run."
|
|
14051
|
+
echo "Fix: ${_LOKI_DELEGATE_FIX}"
|
|
14052
|
+
echo "${_LOKI_DELEGATE_WHY}"
|
|
14750
14053
|
fi
|
|
14751
|
-
|
|
14752
|
-
# first build with a copy-paste command, so they never dead-end here. Gated
|
|
14753
|
-
# on fail_count==0 above, so a failing setup is never told to build. The
|
|
14754
|
-
# --json path returns long before this code, so machine output is untouched.
|
|
14755
|
-
echo ""
|
|
14756
|
-
if [ "$_doctor_sdk_only" = "1" ]; then
|
|
14757
|
-
# SDK-only host: quickstart and demo stay on the bash route and need a
|
|
14758
|
-
# binary on PATH, so recommending them here is a green doctor pointing at
|
|
14759
|
-
# an exit 2. Recommend only what actually runs, plus the one command that
|
|
14760
|
-
# unlocks the rest.
|
|
14761
|
-
echo "Next: loki start ./prd.md (runs on the bundled SDK, no CLI install needed)"
|
|
14762
|
-
echo " For loki quickstart/demo: npm install -g @anthropic-ai/claude-code"
|
|
14763
|
-
else
|
|
14764
|
-
echo "Next: loki quickstart (guided first build from your idea, no PRD needed)"
|
|
14765
|
-
echo " or loki demo (builds a sample todo app end to end) or loki start ./prd.md"
|
|
14766
|
-
fi
|
|
14767
|
-
# A-123: the last stdout line. Provider and model come from the selected
|
|
14768
|
-
# provider; the key state is read WITHOUT creating a key (auto_generate=False).
|
|
14769
|
-
# Byte-mirrored in loki-ts/src/commands/doctor.ts.
|
|
14770
|
-
local _rd_id _rd_model _rd_key _rd_state
|
|
14771
|
-
_rd_id="${LOKI_PROVIDER:-}"
|
|
14772
|
-
[ -n "$_rd_id" ] || _rd_id="$(effective_provider_id 2>/dev/null || true)"
|
|
14773
|
-
[ -n "$_rd_id" ] || _rd_id="claude"
|
|
14774
|
-
_rd_model="$(bash -c 'source "$1" >/dev/null 2>&1; printf %s "${PROVIDER_MODEL_DEVELOPMENT:-}"' _ "${_LOKI_SCRIPT_DIR}/../providers/${_rd_id}.sh" 2>/dev/null || true)"
|
|
14775
|
-
[ -n "$_rd_model" ] || _rd_model="default"
|
|
14776
|
-
_rd_key="$(python3 -E - "${_LOKI_SCRIPT_DIR}" 2>/dev/null <<'PY' || true
|
|
14777
|
-
import os, sys
|
|
14778
|
-
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
14779
|
-
sys.path.insert(0, sys.argv[1])
|
|
14780
|
-
from receipt_jwt import load_signing_key, _CRYPTO_AVAILABLE, RECEIPT_SIGNER_BASENAME
|
|
14781
|
-
if not _CRYPTO_AVAILABLE:
|
|
14782
|
-
print("nocrypto")
|
|
14783
|
-
else:
|
|
14784
|
-
k, kid = load_signing_key(auto_generate=False)
|
|
14785
|
-
if kid:
|
|
14786
|
-
print("kid " + kid[:8])
|
|
14787
|
-
elif os.environ.get("LOKI_RECEIPT_SIGNING_KEY", "").strip():
|
|
14788
|
-
print("bad")
|
|
14789
|
-
else:
|
|
14790
|
-
# A-122: a missing key FILE is auto-generated on first use when its nearest
|
|
14791
|
-
# existing ancestor directory is writable; an existing file that did not
|
|
14792
|
-
# load (corrupt/unreadable) or an unwritable location is unusable.
|
|
14793
|
-
f = os.environ.get("LOKI_RECEIPT_SIGNING_KEY_FILE", "").strip() or os.path.expanduser("~/.loki/keys/receipt-ed25519.pem")
|
|
14794
|
-
d = os.path.dirname(os.path.abspath(f))
|
|
14795
|
-
while not os.path.isdir(d) and os.path.dirname(d) != d:
|
|
14796
|
-
d = os.path.dirname(d)
|
|
14797
|
-
print("bad" if os.path.lexists(f) or not os.access(d, os.W_OK | os.X_OK) else "none")
|
|
14798
|
-
PY
|
|
14799
|
-
)"
|
|
14800
|
-
case "$_rd_key" in
|
|
14801
|
-
"kid "*) _rd_state="receipts signed (${_rd_key})" ;;
|
|
14802
|
-
none) _rd_state="receipts will be signed on first run" ;;
|
|
14803
|
-
bad) _rd_state="receipts unsigned: signing key could not be loaded" ;;
|
|
14804
|
-
*) _rd_state="receipts unsigned: python3 cryptography package missing" ;;
|
|
14805
|
-
esac
|
|
14806
|
-
echo "Ready: ${_rd_id} (${_rd_model}), ${_rd_state}"
|
|
14807
|
-
# Best-effort stale-install nudge (stderr only; never blocks; off on
|
|
14808
|
-
# non-TTY/CI). Reached only on the human-readable path (--json returns above).
|
|
14809
|
-
maybe_print_update_hint
|
|
14810
|
-
return 0
|
|
14054
|
+
return 1
|
|
14811
14055
|
}
|
|
14812
14056
|
|
|
14813
|
-
#
|
|
14057
|
+
# Minimal --json fallback when the Bun doctor is unavailable. Same top-level keys as
|
|
14058
|
+
# DoctorJson (loki-ts/src/commands/doctor.ts) with summary.ok=false, failed=1, plus
|
|
14059
|
+
# route:"unavailable" and the reason.
|
|
14814
14060
|
cmd_doctor_json() {
|
|
14815
|
-
local
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
if declare -f provider_availability_json >/dev/null 2>&1; then
|
|
14824
|
-
_provider_availability="$(provider_availability_json 2>/dev/null || true)"
|
|
14825
|
-
fi
|
|
14826
|
-
LOKI_PROVIDER_AVAILABILITY="$_provider_availability" \
|
|
14827
|
-
LOKI_VERSION="$_loki_version" \
|
|
14828
|
-
LOKI_CATALOG_PATH="${LOKI_MODEL_CATALOG:-${_LOKI_SCRIPT_DIR}/../providers/model_catalog.json}" \
|
|
14829
|
-
python3 -c "
|
|
14830
|
-
import json, os, subprocess, sys, shutil
|
|
14831
|
-
|
|
14832
|
-
def get_version(cmd, args=None):
|
|
14833
|
-
try:
|
|
14834
|
-
if args is None:
|
|
14835
|
-
args = ['--version']
|
|
14836
|
-
result = subprocess.run([cmd] + args, capture_output=True, text=True, timeout=5)
|
|
14837
|
-
output = result.stdout.strip() or result.stderr.strip()
|
|
14838
|
-
# Extract version number
|
|
14839
|
-
import re
|
|
14840
|
-
match = re.search(r'(\d+\.\d+[\.\d]*)', output)
|
|
14841
|
-
return match.group(1) if match else None
|
|
14842
|
-
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
14843
|
-
return None
|
|
14844
|
-
|
|
14845
|
-
def check_tool(name, cmd, required, min_version=None):
|
|
14846
|
-
found = shutil.which(cmd) is not None
|
|
14847
|
-
version = get_version(cmd) if found else None
|
|
14848
|
-
status = 'pass'
|
|
14849
|
-
|
|
14850
|
-
if not found:
|
|
14851
|
-
status = 'fail' if required == 'required' else 'warn'
|
|
14852
|
-
elif min_version and version:
|
|
14853
|
-
cur_parts = [int(x) for x in version.split('.')[:2]]
|
|
14854
|
-
min_parts = [int(x) for x in min_version.split('.')[:2]]
|
|
14855
|
-
while len(cur_parts) < 2: cur_parts.append(0)
|
|
14856
|
-
while len(min_parts) < 2: min_parts.append(0)
|
|
14857
|
-
if cur_parts < min_parts:
|
|
14858
|
-
status = 'fail' if required == 'required' else 'warn'
|
|
14859
|
-
|
|
14860
|
-
return {
|
|
14861
|
-
'name': name,
|
|
14862
|
-
'command': cmd,
|
|
14863
|
-
'found': found,
|
|
14864
|
-
'version': version,
|
|
14865
|
-
'required': required,
|
|
14866
|
-
'min_version': min_version,
|
|
14867
|
-
'status': status,
|
|
14868
|
-
'path': shutil.which(cmd)
|
|
14869
|
-
}
|
|
14870
|
-
|
|
14871
|
-
checks = []
|
|
14872
|
-
checks.append(check_tool('Node.js', 'node', 'required', '18.0'))
|
|
14873
|
-
checks.append(check_tool('Python 3', 'python3', 'required', '3.8'))
|
|
14874
|
-
checks.append(check_tool('jq', 'jq', 'required'))
|
|
14875
|
-
checks.append(check_tool('git', 'git', 'required'))
|
|
14876
|
-
checks.append(check_tool('curl', 'curl', 'required'))
|
|
14877
|
-
checks.append(check_tool('bash', 'bash', 'recommended', '4.0'))
|
|
14878
|
-
checks.append(check_tool('Bun', 'bun', 'recommended', '1.3'))
|
|
14879
|
-
checks.append(check_tool('Claude CLI', 'claude', 'optional'))
|
|
14880
|
-
checks.append(check_tool('Codex CLI', 'codex', 'optional'))
|
|
14881
|
-
checks.append(check_tool('Cline CLI', 'cline', 'optional'))
|
|
14882
|
-
checks.append(check_tool('Aider CLI', 'aider', 'optional'))
|
|
14883
|
-
checks.append(check_tool('opencode CLI', 'opencode', 'optional'))
|
|
14884
|
-
|
|
14885
|
-
# Disk space
|
|
14886
|
-
disk_gb = None
|
|
14887
|
-
try:
|
|
14888
|
-
stat = os.statvfs(os.path.expanduser('~'))
|
|
14889
|
-
disk_gb = round((stat.f_bavail * stat.f_frsize) / (1024**3), 1)
|
|
14890
|
-
except Exception:
|
|
14891
|
-
pass
|
|
14892
|
-
|
|
14893
|
-
disk_status = 'pass'
|
|
14894
|
-
if disk_gb is not None:
|
|
14895
|
-
if disk_gb < 1:
|
|
14896
|
-
disk_status = 'fail'
|
|
14897
|
-
elif disk_gb < 5:
|
|
14898
|
-
disk_status = 'warn'
|
|
14899
|
-
|
|
14900
|
-
# v7.5.15: expose the sentrux architectural-drift gate state in --json so
|
|
14901
|
-
# dashboards/automation can surface it the same way the text-mode block does
|
|
14902
|
-
# (added in v7.5.14). Sibling of checks/disk -- intentionally not counted in
|
|
14903
|
-
# the summary tally to keep summary numbers backwards-compatible.
|
|
14904
|
-
sentrux_found = shutil.which('sentrux') is not None
|
|
14905
|
-
sentrux_version = get_version('sentrux') if sentrux_found else None
|
|
14906
|
-
sentrux_status = 'pass' if sentrux_found else 'warn'
|
|
14907
|
-
sentrux = {
|
|
14908
|
-
'found': sentrux_found,
|
|
14909
|
-
'version': sentrux_version,
|
|
14910
|
-
'status': sentrux_status,
|
|
14911
|
-
'required': 'optional'
|
|
14912
|
-
}
|
|
14913
|
-
|
|
14914
|
-
# v7.7.17: memory subsystem health surface. Reports the latest entries
|
|
14915
|
-
# from .loki/memory/.errors.log (rotated by memory/error_log.py) so
|
|
14916
|
-
# developers see regressions in the previously-silent-fail call sites
|
|
14917
|
-
# at run.sh:8724, 9087, 9136. Sibling of checks/disk/sentrux; not
|
|
14918
|
-
# counted in the summary tally to keep backwards-compatible numbers.
|
|
14919
|
-
# Inline file read (rather than importing memory.error_log) so this
|
|
14920
|
-
# block works regardless of the doctor's invocation cwd / sys.path.
|
|
14921
|
-
# v7.7.17 council fix (Opus 2): tail-only read (last 64 KB) so a
|
|
14922
|
-
# pathological oversize log cannot OOM the doctor command. Bash + Bun
|
|
14923
|
-
# emit the SAME relative path string for parity.
|
|
14924
|
-
memory_base = os.path.join(os.environ.get('LOKI_DIR', '.loki'), 'memory')
|
|
14925
|
-
_memory_log_path = os.path.join(memory_base, '.errors.log')
|
|
14926
|
-
_TAIL_BYTES = 64 * 1024
|
|
14927
|
-
memory_recent_errors = []
|
|
14928
|
-
if os.path.isfile(_memory_log_path):
|
|
14929
|
-
try:
|
|
14930
|
-
with open(_memory_log_path, 'rb') as _f:
|
|
14931
|
-
_f.seek(0, 2)
|
|
14932
|
-
_size = _f.tell()
|
|
14933
|
-
_offset = max(0, _size - _TAIL_BYTES)
|
|
14934
|
-
_f.seek(_offset)
|
|
14935
|
-
_chunk = _f.read()
|
|
14936
|
-
_text = _chunk.decode('utf-8', errors='replace')
|
|
14937
|
-
_lines = _text.split('\n')
|
|
14938
|
-
if _offset > 0 and _lines:
|
|
14939
|
-
_lines = _lines[1:] # drop possibly-partial first line
|
|
14940
|
-
_lines = [ln.strip() for ln in _lines if ln.strip()]
|
|
14941
|
-
memory_recent_errors = _lines[-5:]
|
|
14942
|
-
except OSError:
|
|
14943
|
-
memory_recent_errors = []
|
|
14944
|
-
memory = {
|
|
14945
|
-
'errors_log_path': _memory_log_path if os.path.isfile(_memory_log_path) else None,
|
|
14946
|
-
'recent_errors': memory_recent_errors,
|
|
14947
|
-
'recent_error_count': len(memory_recent_errors),
|
|
14948
|
-
'status': 'pass' if not memory_recent_errors else 'warn',
|
|
14949
|
-
}
|
|
14950
|
-
|
|
14951
|
-
# SKILL LINK INTEGRITY. The text path has always failed closed on a broken
|
|
14952
|
-
# skill symlink (the Skills section, _doctor_block, exit 1), but --json
|
|
14953
|
-
# omitted skills entirely -- so on a host with a dangling ~/.claude/skills/
|
|
14954
|
-
# loki-mode the two outputs gave OPPOSITE verdicts: text exited 1 while
|
|
14955
|
-
# --json reported failed 0 and ok true. An operator gating on the JSON got a
|
|
14956
|
-
# green on a host where the skill cannot load. Same class of fake-green as
|
|
14957
|
-
# the ai_provider block below, and fixed the same way: counted in the tally.
|
|
14958
|
-
#
|
|
14959
|
-
# Per-entry counting matches the text path, which tallies each of the four
|
|
14960
|
-
# entries separately. Paths are the same ones the text path already prints,
|
|
14961
|
-
# so this exposes nothing new.
|
|
14962
|
-
#
|
|
14963
|
-
# CAUTION: this block lives inside a DOUBLE-QUOTED python3 -c program, so no
|
|
14964
|
-
# apostrophes and no double quotes anywhere here, comments included.
|
|
14965
|
-
#
|
|
14966
|
-
# SEVERITY MIRRORS THE TEXT PATH: a dangling link is a failure only for the
|
|
14967
|
-
# provider a build would actually use. Read from the SAME payload the text
|
|
14968
|
-
# route uses (LOKI_PROVIDER_AVAILABILITY, set by cmd_doctor_json), parsed here
|
|
14969
|
-
# rather than via a second env var so the two cannot disagree. LOKI_PROVIDER
|
|
14970
|
-
# wins for the same reason effective_provider_id honors it.
|
|
14971
|
-
_SKILL_ENTRIES = (
|
|
14972
|
-
('Claude Code', '.claude/skills/loki-mode', 'claude'),
|
|
14973
|
-
('Codex CLI', '.codex/skills/loki-mode', 'codex'),
|
|
14974
|
-
('Cline CLI', '.cline/skills/loki-mode', 'cline'),
|
|
14975
|
-
('Aider CLI', '.aider/skills/loki-mode', 'aider'),
|
|
14976
|
-
)
|
|
14977
|
-
try:
|
|
14978
|
-
_sk_pa = os.environ.get('LOKI_PROVIDER_AVAILABILITY', '').strip()
|
|
14979
|
-
_sk_selected = (json.loads(_sk_pa).get('selected') or '') if _sk_pa else ''
|
|
14980
|
-
except Exception:
|
|
14981
|
-
_sk_selected = ''
|
|
14982
|
-
_sk_selected = os.environ.get('LOKI_PROVIDER', '') or _sk_selected
|
|
14983
|
-
skills = []
|
|
14984
|
-
for _sk_name, _sk_rel, _sk_id in _SKILL_ENTRIES:
|
|
14985
|
-
_sk_dir = os.path.join(os.path.expanduser('~'), _sk_rel)
|
|
14986
|
-
# JSON is commonly archived or forwarded to automation. Keep the useful
|
|
14987
|
-
# user-relative location without exposing the machine-specific home path.
|
|
14988
|
-
_sk_json_path = '~/' + _sk_rel
|
|
14989
|
-
if os.path.isfile(os.path.join(_sk_dir, 'SKILL.md')):
|
|
14990
|
-
skills.append({'name': _sk_name, 'path': _sk_json_path, 'status': 'pass',
|
|
14991
|
-
'detail': None, 'required': 'required'})
|
|
14992
|
-
elif os.path.islink(_sk_dir):
|
|
14993
|
-
# islink is true for a DANGLING link (lstat-based), which is exactly
|
|
14994
|
-
# the broken case the text path reports. fail only for the effective
|
|
14995
|
-
# provider; otherwise warn, matching the text path badge for badge.
|
|
14996
|
-
# 'required' stays 'required' in both arms: an absent link has always
|
|
14997
|
-
# been warn+required, so the warn arm introduces no new shape, and a
|
|
14998
|
-
# consumer keys on 'status'.
|
|
14999
|
-
_sk_blocking = bool(_sk_selected) and _sk_id == _sk_selected
|
|
15000
|
-
skills.append({'name': _sk_name, 'path': _sk_json_path,
|
|
15001
|
-
'status': 'fail' if _sk_blocking else 'warn',
|
|
15002
|
-
'detail': 'broken symlink. Fix: loki setup-skill',
|
|
15003
|
-
'required': 'required'})
|
|
15004
|
-
else:
|
|
15005
|
-
skills.append({'name': _sk_name, 'path': _sk_json_path, 'status': 'warn',
|
|
15006
|
-
'detail': 'not found - run loki setup-skill',
|
|
15007
|
-
'required': 'required'})
|
|
15008
|
-
|
|
15009
|
-
pass_count = sum(1 for c in checks if c['status'] == 'pass')
|
|
15010
|
-
fail_count = sum(1 for c in checks if c['status'] == 'fail')
|
|
15011
|
-
warn_count = sum(1 for c in checks if c['status'] == 'warn')
|
|
15012
|
-
|
|
15013
|
-
for _sk in skills:
|
|
15014
|
-
if _sk['status'] == 'pass':
|
|
15015
|
-
pass_count += 1
|
|
15016
|
-
elif _sk['status'] == 'fail':
|
|
15017
|
-
fail_count += 1
|
|
15018
|
-
else:
|
|
15019
|
-
warn_count += 1
|
|
15020
|
-
|
|
15021
|
-
if disk_status == 'pass': pass_count += 1
|
|
15022
|
-
elif disk_status == 'fail': fail_count += 1
|
|
15023
|
-
elif disk_status == 'warn': warn_count += 1
|
|
15024
|
-
|
|
15025
|
-
# AGGREGATE PROVIDER CHECK. Each provider CLI is individually optional --
|
|
15026
|
-
# you need one of Claude, Codex, Cline, Aider, or opencode -- so no single
|
|
15027
|
-
# one can be marked required. But having NONE is a blocker, and the text
|
|
15028
|
-
# path reports it as one (the No-AI-provider branch near autonomy/loki:11000).
|
|
15029
|
-
#
|
|
15030
|
-
# --json omitted this entirely, so on a host with no provider the two
|
|
15031
|
-
# outputs gave OPPOSITE verdicts: the text path exited 1 while --json
|
|
15032
|
-
# reported zero failures and ok true. An operator gating a pipeline on the
|
|
15033
|
-
# JSON got a pass on a machine that cannot run a build -- the fake-green
|
|
15034
|
-
# this project exists to refuse.
|
|
15035
|
-
#
|
|
15036
|
-
# CAUTION: this block lives inside python3 -c with a DOUBLE-QUOTED program,
|
|
15037
|
-
# so an apostrophe or a double quote in a comment terminates the shell
|
|
15038
|
-
# string and mangles the program. First attempt did exactly that: doctor
|
|
15039
|
-
# --json printed nothing and exited 0. Keep this comment quote-free.
|
|
15040
|
-
# Must match auto_detect_provider() in providers/loader.sh, in ITS order.
|
|
15041
|
-
# Omitting opencode made doctor report 'No AI provider CLI' on a machine
|
|
15042
|
-
# where the runner would happily select opencode -- the fifth list in this
|
|
15043
|
-
# repo to drift from that authority (see v8.76.0, v8.82.0).
|
|
15044
|
-
_provider_cmds = ('claude', 'cline', 'codex', 'aider', 'opencode')
|
|
15045
|
-
_any_provider = any(shutil.which(_p) is not None for _p in _provider_cmds)
|
|
15046
|
-
ai_provider = {
|
|
15047
|
-
'found': _any_provider,
|
|
15048
|
-
'status': 'pass' if _any_provider else 'fail',
|
|
15049
|
-
'required': 'required',
|
|
15050
|
-
'detail': None if _any_provider
|
|
15051
|
-
else 'No AI provider CLI. Fix: npm install -g @anthropic-ai/claude-code',
|
|
15052
|
-
}
|
|
15053
|
-
if _any_provider:
|
|
15054
|
-
pass_count += 1
|
|
15055
|
-
else:
|
|
15056
|
-
fail_count += 1
|
|
15057
|
-
|
|
15058
|
-
# Model catalog freshness. Local read only -- no network. Advisory: deliberately
|
|
15059
|
-
# excluded from pass/fail/warn counts and from 'ok', so a stale catalog can never
|
|
15060
|
-
# flip doctor's exit code. Mirrors describeCatalogFreshness() in
|
|
15061
|
-
# loki-ts/src/commands/doctor.ts.
|
|
15062
|
-
CATALOG_STALE_DAYS = 90
|
|
15063
|
-
_cat_path = os.environ['LOKI_CATALOG_PATH']
|
|
15064
|
-
try:
|
|
15065
|
-
import datetime as _dt
|
|
15066
|
-
_cat_updated = json.load(open(_cat_path))['updated']
|
|
15067
|
-
_cat_age = (_dt.datetime.now(_dt.timezone.utc).date() - _dt.date.fromisoformat(_cat_updated)).days
|
|
15068
|
-
if _cat_age > CATALOG_STALE_DAYS:
|
|
15069
|
-
# Deliberately NOT counted. The block comment above states catalog age is
|
|
15070
|
-
# excluded from pass/fail/warn and from 'ok' so a stale catalog can never
|
|
15071
|
-
# fail a build -- this increment contradicted that contract, and it also
|
|
15072
|
-
# broke route parity: the Bun route (doctor.ts) never counted it, so the
|
|
15073
|
-
# same host reported warnings: 2 on bash and 1 on Bun. bun-parity
|
|
15074
|
-
# compares these byte for byte, so it would have gone red on day 91.
|
|
15075
|
-
model_catalog = {'status': 'warn', 'updated': _cat_updated, 'age_days': _cat_age,
|
|
15076
|
-
'detail': f'Last updated {_cat_updated} ({_cat_age} days ago) -- may be missing newer models'}
|
|
15077
|
-
else:
|
|
15078
|
-
model_catalog = {'status': 'pass', 'updated': _cat_updated, 'age_days': _cat_age,
|
|
15079
|
-
'detail': f'Last updated {_cat_updated} ({_cat_age} days ago)'}
|
|
15080
|
-
except Exception:
|
|
15081
|
-
model_catalog = {'status': 'warn', 'updated': None, 'age_days': None,
|
|
15082
|
-
'detail': 'Catalog unreadable or missing an ISO \"updated\" date -- cannot determine age'}
|
|
15083
|
-
|
|
15084
|
-
# Provider availability, handed in from bash (see cmd_doctor_json). Null when
|
|
15085
|
-
# providers/loader.sh could not be sourced, mirroring the text sections skip.
|
|
15086
|
-
# CAUTION: this block lives inside a DOUBLE-QUOTED python3 -c program, so no
|
|
15087
|
-
# apostrophes and no double quotes anywhere here, comments included.
|
|
15088
|
-
try:
|
|
15089
|
-
_pa_raw = os.environ.get('LOKI_PROVIDER_AVAILABILITY', '').strip()
|
|
15090
|
-
provider_availability = json.loads(_pa_raw) if _pa_raw else None
|
|
15091
|
-
except Exception:
|
|
15092
|
-
provider_availability = None
|
|
15093
|
-
|
|
15094
|
-
result = {
|
|
15095
|
-
'loki_mode_version': os.environ.get('LOKI_VERSION', 'unknown'),
|
|
15096
|
-
'checks': checks,
|
|
15097
|
-
'provider_availability': provider_availability,
|
|
15098
|
-
'disk': {
|
|
15099
|
-
'available_gb': disk_gb,
|
|
15100
|
-
'status': disk_status
|
|
15101
|
-
},
|
|
15102
|
-
'ai_provider': ai_provider,
|
|
15103
|
-
'skills': skills,
|
|
15104
|
-
'sentrux': sentrux,
|
|
15105
|
-
'memory': memory,
|
|
15106
|
-
'model_catalog': model_catalog,
|
|
15107
|
-
'summary': {
|
|
15108
|
-
'passed': pass_count,
|
|
15109
|
-
'failed': fail_count,
|
|
15110
|
-
'warnings': warn_count,
|
|
15111
|
-
'ok': fail_count == 0
|
|
15112
|
-
}
|
|
15113
|
-
}
|
|
15114
|
-
|
|
15115
|
-
print(json.dumps(result, indent=2))
|
|
15116
|
-
sys.exit(0 if result['summary']['ok'] else 1)
|
|
15117
|
-
"
|
|
14061
|
+
local ver why fix
|
|
14062
|
+
ver="$(get_version 2>/dev/null || echo unknown)"
|
|
14063
|
+
why="${_LOKI_DELEGATE_WHY:-bun route unavailable}"
|
|
14064
|
+
fix="${_LOKI_DELEGATE_FIX:-}"
|
|
14065
|
+
why="${why//\\/\\\\}"; why="${why//\"/\\\"}"
|
|
14066
|
+
fix="${fix//\\/\\\\}"; fix="${fix//\"/\\\"}"
|
|
14067
|
+
ver="${ver//\\/\\\\}"; ver="${ver//\"/\\\"}"
|
|
14068
|
+
printf '{"loki_mode_version":"%s","checks":[],"provider_availability":null,"disk":null,"ai_provider":null,"skills":[],"sentrux":null,"memory":null,"model_catalog":null,"summary":{"passed":0,"failed":1,"warnings":0,"ok":false},"route":"unavailable","reason":"%s","fix":"%s"}\n' "$ver" "$why" "$fix"
|
|
15118
14069
|
}
|
|
15119
14070
|
|
|
15120
14071
|
# Architectural-drift gate (v7.5.14, opt-in).
|
|
@@ -37656,7 +36607,11 @@ print(json.dumps(json.load(open(os.environ['LOKI_PROOF_JSON'])), indent=2))
|
|
|
37656
36607
|
echo -e "${YELLOW}NOT CHECKED${NC}: python3 is not on PATH, so the verifier could not run (exit 2)." >&2
|
|
37657
36608
|
exit 2
|
|
37658
36609
|
fi
|
|
37659
|
-
local _pv_rc=0 _pv_out="" _pv_base_rc=0
|
|
36610
|
+
local _pv_rc=0 _pv_out="" _pv_base_rc=0 _pv_root=""
|
|
36611
|
+
_pv_root="$(loki_verify_root)" || {
|
|
36612
|
+
echo -e "${YELLOW}NOT CHECKED${NC}: no project tree to verify against (exit 2)." >&2
|
|
36613
|
+
exit 2
|
|
36614
|
+
}
|
|
37660
36615
|
# Captured, not printed directly, so that --jwks (below) can patch
|
|
37661
36616
|
# it: BACKLOG 49 -- when --jwks later downgrades _pv_rc to 1
|
|
37662
36617
|
# (ABSENT) or 2 (NOT CHECKED), this output's own "ok: true"/
|
|
@@ -37666,9 +36621,9 @@ print(json.dumps(json.load(open(os.environ['LOKI_PROOF_JSON'])), indent=2))
|
|
|
37666
36621
|
# route (proof.ts verifyProof) -- ok already agrees with the exit
|
|
37667
36622
|
# code there, so there is nothing to patch.
|
|
37668
36623
|
if [ "$_pv_human" -eq 1 ]; then
|
|
37669
|
-
_pv_out="$(python3 -E "$verifier" --human "$pj" "$
|
|
36624
|
+
_pv_out="$(python3 -E "$verifier" --human "$pj" "$_pv_root")" || _pv_rc=$?
|
|
37670
36625
|
else
|
|
37671
|
-
_pv_out="$(python3 -E "$verifier" "$pj" "$
|
|
36626
|
+
_pv_out="$(python3 -E "$verifier" "$pj" "$_pv_root")" || _pv_rc=$?
|
|
37672
36627
|
fi
|
|
37673
36628
|
_pv_base_rc="$_pv_rc"
|
|
37674
36629
|
|