loki-mode 7.28.2 → 7.30.0
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/README.md +4 -3
- package/SKILL.md +8 -2
- package/VERSION +1 -1
- package/autonomy/loki +285 -26
- package/autonomy/mcp-launch.sh +282 -0
- package/autonomy/provider-offer.sh +249 -0
- package/autonomy/quickstart.sh +584 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +10 -1
- package/docs/competitive/emergence-others-analysis.md +1 -1
- package/docs/competitive/replit-lovable-analysis.md +2 -2
- package/loki-ts/dist/loki.js +208 -208
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +186 -35
- package/package.json +1 -1
- package/templates/simple-todo-app.md +3 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# mcp-launch.sh -- launch the Loki Mode MCP server, bootstrapping its Python
|
|
3
|
+
# dependencies on first run (task 562).
|
|
4
|
+
#
|
|
5
|
+
# Why this exists: a fresh `npm install -g loki-mode` ships mcp/server.py and
|
|
6
|
+
# mcp/requirements.txt but installs NO Python packages and exposes only the
|
|
7
|
+
# `loki` bin. So `python3 -m mcp.server` exits because the MCP SDK is absent.
|
|
8
|
+
# `loki mcp` closes that gap: it checks for python3 + the MCP SDK, and when the
|
|
9
|
+
# SDK is missing it offers a consent-gated bootstrap into a project-local
|
|
10
|
+
# virtualenv (.loki/mcp-venv), then execs the server over stdio using THAT
|
|
11
|
+
# venv's python so the SDK is actually importable.
|
|
12
|
+
#
|
|
13
|
+
# Design (least-invasive, honest):
|
|
14
|
+
# * Venv location: <user-cwd>/.loki/mcp-venv (the project Loki is invoked in,
|
|
15
|
+
# NOT the install root). Project-local, no global site-packages pollution,
|
|
16
|
+
# no sudo, no curl-pipe-bash, no root-owned writes under a global install.
|
|
17
|
+
# Removing the project's .loki fully uninstalls. Override with
|
|
18
|
+
# LOKI_MCP_VENV=/abs/path. Honors LOKI_DIR (defaults to .loki).
|
|
19
|
+
# * The server is launched with PYTHONPATH set to the install root (NOT by
|
|
20
|
+
# cd-ing into it) so the user's cwd is preserved: mcp/server.py resolves the
|
|
21
|
+
# project .loki from os.getcwd(). Without PYTHONPATH, `import mcp` from an
|
|
22
|
+
# arbitrary cwd resolves to the pip MCP SDK's own `mcp` package (zero Loki
|
|
23
|
+
# tools); PYTHONPATH puts the install root first so the LOCAL mcp/server.py
|
|
24
|
+
# wins, while server.py's namespace juggle still hands the real SDK its own
|
|
25
|
+
# `mcp.*` subtree.
|
|
26
|
+
# * The ONLY command run on the user's behalf is, after explicit consent:
|
|
27
|
+
# <venv>/bin/pip install -r mcp/requirements.txt
|
|
28
|
+
# The exact command is printed before it runs.
|
|
29
|
+
# * Non-interactive / CI: NEVER install. Print the manual command to stderr
|
|
30
|
+
# and exit 2 (mirrors autonomy/provider-offer.sh gate semantics).
|
|
31
|
+
# * Opt-out: LOKI_NO_INSTALL_OFFER=1 -> never prompt, print manual command,
|
|
32
|
+
# exit 2. --yes / LOKI_ASSUME_YES / LOKI_AUTO_CONFIRM=true -> auto-accept.
|
|
33
|
+
#
|
|
34
|
+
# Self-containment: depends only on bash builtins + python3 on PATH. Defines
|
|
35
|
+
# its own colors so it behaves identically whether sourced by autonomy/loki or
|
|
36
|
+
# run standalone.
|
|
37
|
+
|
|
38
|
+
# Guard against double-source.
|
|
39
|
+
if [ -n "${_LOKI_MCP_LAUNCH_SOURCED:-}" ]; then
|
|
40
|
+
return 0 2>/dev/null || true
|
|
41
|
+
fi
|
|
42
|
+
_LOKI_MCP_LAUNCH_SOURCED=1
|
|
43
|
+
|
|
44
|
+
# --- Self-contained colors (honor NO_COLOR) --------------------------------
|
|
45
|
+
if [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then
|
|
46
|
+
_ML_RED=''; _ML_YELLOW=''; _ML_BOLD=''; _ML_NC=''
|
|
47
|
+
else
|
|
48
|
+
_ML_RED=$'\033[0;31m'
|
|
49
|
+
_ML_YELLOW=$'\033[1;33m'
|
|
50
|
+
_ML_BOLD=$'\033[1m'
|
|
51
|
+
_ML_NC=$'\033[0m'
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Repo root = parent of the directory holding this script (autonomy/..).
|
|
55
|
+
_ml_repo_root() {
|
|
56
|
+
local self_dir
|
|
57
|
+
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
58
|
+
(cd "$self_dir/.." && pwd)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# _ml_assume_yes: true when the user opted into unattended confirmation.
|
|
62
|
+
_ml_assume_yes() {
|
|
63
|
+
[ "${LOKI_ASSUME_YES:-}" = "1" ] && return 0
|
|
64
|
+
[ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 0
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# _ml_non_interactive: true when we must NEVER prompt (non-TTY or CI).
|
|
69
|
+
_ml_non_interactive() {
|
|
70
|
+
[ ! -t 0 ] && return 0
|
|
71
|
+
[ ! -t 1 ] && return 0
|
|
72
|
+
[ -n "${CI:-}" ] && return 0
|
|
73
|
+
return 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# _ml_python: echo the best base python3 for creating the venv, or empty.
|
|
77
|
+
_ml_python() {
|
|
78
|
+
local p
|
|
79
|
+
for p in python3 python3.12 python3.11; do
|
|
80
|
+
if command -v "$p" >/dev/null 2>&1; then
|
|
81
|
+
printf '%s' "$p"
|
|
82
|
+
return 0
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# _ml_sdk_importable <python> <root>: true (0) only if the real MCP SDK's
|
|
89
|
+
# FastMCP can actually be CONSTRUCTED -- not merely that the SDK files exist on
|
|
90
|
+
# disk. A file-exists check is a false positive: under the local-vs-SDK `mcp`
|
|
91
|
+
# namespace collision the package-dir FastMCP can be present yet fail to import
|
|
92
|
+
# (`No module named 'mcp.types'`). We delegate to mcp/server.py's own
|
|
93
|
+
# `--check-sdk` probe, which runs the exact loader the server uses and exits 0
|
|
94
|
+
# only when FastMCP loaded.
|
|
95
|
+
#
|
|
96
|
+
# Critical: we set PYTHONPATH to the install root and DO NOT cd into it, so the
|
|
97
|
+
# probe exercises the SAME module resolution as the real launch (which preserves
|
|
98
|
+
# the user's cwd). The redirect of stdin from /dev/null is insurance: if the
|
|
99
|
+
# pip SDK's own `mcp.server` were ever reached, its stub starts a stdio receive
|
|
100
|
+
# loop; the EOF makes it exit instead of hanging.
|
|
101
|
+
_ml_sdk_importable() {
|
|
102
|
+
local py="$1" root="$2"
|
|
103
|
+
PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" \
|
|
104
|
+
"$py" -m mcp.server --check-sdk </dev/null >/dev/null 2>&1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# _ml_print_manual <root> <venv>: print the honest manual install commands.
|
|
108
|
+
# The venv lives in the user's project (.loki/mcp-venv by default), while
|
|
109
|
+
# requirements.txt is shipped under the install root.
|
|
110
|
+
_ml_print_manual() {
|
|
111
|
+
local root="$1" venv="$2"
|
|
112
|
+
printf 'Install the MCP server dependencies manually:\n' >&2
|
|
113
|
+
printf ' python3 -m venv %s\n' "$venv" >&2
|
|
114
|
+
printf ' %s/bin/pip install -r %s/mcp/requirements.txt\n' "$venv" "$root" >&2
|
|
115
|
+
printf ' PYTHONPATH=%s %s/bin/python -m mcp.server\n' "$root" "$venv" >&2
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_ml_help() {
|
|
119
|
+
cat <<'EOF'
|
|
120
|
+
Loki Mode -- launch the MCP (Model Context Protocol) server
|
|
121
|
+
|
|
122
|
+
Usage: loki mcp [--transport stdio|http] [--port N] [--help]
|
|
123
|
+
|
|
124
|
+
Starts the Loki Mode MCP server so MCP-aware clients (Claude Code, IDEs)
|
|
125
|
+
can call Loki's tools (memory, task queue, code search, build management).
|
|
126
|
+
|
|
127
|
+
On first run, if the Python MCP SDK is not installed, Loki offers to create
|
|
128
|
+
a project-local virtualenv at .loki/mcp-venv and install mcp/requirements.txt
|
|
129
|
+
into it (with your consent). The exact pip command is printed before it runs.
|
|
130
|
+
It then launches the server using that venv's python.
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
--transport stdio|http Transport to use (default: stdio).
|
|
134
|
+
--port N Port for http transport (default: 8421).
|
|
135
|
+
--help, -h Show this help and exit.
|
|
136
|
+
|
|
137
|
+
Environment:
|
|
138
|
+
LOKI_MCP_VENV=/abs/path Use a custom venv location instead of .loki/mcp-venv.
|
|
139
|
+
LOKI_NO_INSTALL_OFFER=1 Never prompt to install; print the manual command.
|
|
140
|
+
--yes / LOKI_ASSUME_YES=1 Auto-accept the dependency install.
|
|
141
|
+
|
|
142
|
+
Behavior in non-interactive / CI shells: never installs. Prints the manual
|
|
143
|
+
install command to stderr and exits 2.
|
|
144
|
+
EOF
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
|
|
148
|
+
mcp_launch_main() {
|
|
149
|
+
# Parse only flags we own; everything else is forwarded to the server.
|
|
150
|
+
local arg
|
|
151
|
+
for arg in "$@"; do
|
|
152
|
+
case "$arg" in
|
|
153
|
+
--help|-h|help)
|
|
154
|
+
_ml_help
|
|
155
|
+
return 0
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
local root
|
|
161
|
+
root="$(_ml_repo_root)"
|
|
162
|
+
|
|
163
|
+
# 1. python3 presence.
|
|
164
|
+
local base_py
|
|
165
|
+
if ! base_py="$(_ml_python)"; then
|
|
166
|
+
printf '%sNo python3 found on PATH.%s The MCP server needs Python 3.\n' "$_ML_RED" "$_ML_NC" >&2
|
|
167
|
+
printf 'Install Python 3 (https://www.python.org/downloads), then re-run: loki mcp\n' >&2
|
|
168
|
+
return 2
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# 2. venv location. Lives in the USER'S project (their cwd), NOT the install
|
|
172
|
+
# root: $root may be a root-owned global npm prefix where we must never
|
|
173
|
+
# write. LOKI_DIR (default .loki) keeps this consistent with every other
|
|
174
|
+
# .loki artifact; LOKI_MCP_VENV overrides outright.
|
|
175
|
+
local venv="${LOKI_MCP_VENV:-$PWD/${LOKI_DIR:-.loki}/mcp-venv}"
|
|
176
|
+
local venv_py="$venv/bin/python"
|
|
177
|
+
|
|
178
|
+
# 3. If the venv already has the SDK, use it directly. The server is launched
|
|
179
|
+
# with PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
|
|
180
|
+
# .loki resolution; see _ml_sdk_importable for why.
|
|
181
|
+
# Known narrow residual: if the user's cwd itself contains a Python
|
|
182
|
+
# package literally named mcp/ with a server submodule, python -m puts
|
|
183
|
+
# the cwd ahead of PYTHONPATH and that package wins. Essentially never
|
|
184
|
+
# true for real projects; documented rather than fought.
|
|
185
|
+
if [ -x "$venv_py" ] && _ml_sdk_importable "$venv_py" "$root"; then
|
|
186
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# 4. If the BASE python already has the SDK (e.g. user pip-installed it),
|
|
190
|
+
# use it -- no venv needed.
|
|
191
|
+
if _ml_sdk_importable "$base_py" "$root"; then
|
|
192
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py" -m mcp.server "$@"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# 5. SDK missing. Decide whether we may bootstrap.
|
|
196
|
+
if [ "${LOKI_NO_INSTALL_OFFER:-}" = "1" ]; then
|
|
197
|
+
printf '%sMCP SDK not installed.%s\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
198
|
+
_ml_print_manual "$root" "$venv"
|
|
199
|
+
return 2
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
if _ml_non_interactive; then
|
|
203
|
+
printf '%sMCP SDK not installed%s and this is a non-interactive shell, so Loki will not install it automatically.\n' "$_ML_YELLOW" "$_ML_NC" >&2
|
|
204
|
+
_ml_print_manual "$root" "$venv"
|
|
205
|
+
return 2
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# 6. Interactive TTY: offer the consent-gated bootstrap.
|
|
209
|
+
local answer=""
|
|
210
|
+
if _ml_assume_yes; then
|
|
211
|
+
answer="y"
|
|
212
|
+
else
|
|
213
|
+
printf '\n'
|
|
214
|
+
printf 'The MCP server needs Python dependencies that are not installed.\n'
|
|
215
|
+
printf 'Loki can create a project-local virtualenv and install them:\n'
|
|
216
|
+
printf ' python3 -m venv %s\n' "$venv"
|
|
217
|
+
printf ' %s/bin/pip install -r %s/mcp/requirements.txt\n' "$venv" "$root"
|
|
218
|
+
printf '\n'
|
|
219
|
+
printf 'Nothing is installed globally and no sudo is used. Proceed? [Y/n] '
|
|
220
|
+
read -r answer || answer="n"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
case "$answer" in
|
|
224
|
+
""|y|Y|yes|YES) ;;
|
|
225
|
+
*)
|
|
226
|
+
printf 'Skipped.\n'
|
|
227
|
+
_ml_print_manual "$root" "$venv"
|
|
228
|
+
return 2
|
|
229
|
+
;;
|
|
230
|
+
esac
|
|
231
|
+
|
|
232
|
+
# 7. Create the venv if needed. Ensure the parent .loki exists in the user's
|
|
233
|
+
# project first (never write under the install root).
|
|
234
|
+
if [ ! -x "$venv_py" ]; then
|
|
235
|
+
local venv_parent
|
|
236
|
+
venv_parent="$(dirname "$venv")"
|
|
237
|
+
if [ ! -d "$venv_parent" ] && ! mkdir -p "$venv_parent"; then
|
|
238
|
+
printf '%sCannot create %s (no write access).%s\n' "$_ML_RED" "$venv_parent" "$_ML_NC" >&2
|
|
239
|
+
_ml_print_manual "$root" "$venv"
|
|
240
|
+
return 2
|
|
241
|
+
fi
|
|
242
|
+
printf 'Creating virtualenv (%s) ...\n' "$venv"
|
|
243
|
+
if ! "$base_py" -m venv "$venv"; then
|
|
244
|
+
printf '%sFailed to create virtualenv at %s.%s\n' "$_ML_RED" "$venv" "$_ML_NC" >&2
|
|
245
|
+
_ml_print_manual "$root" "$venv"
|
|
246
|
+
return 2
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# 8. Install requirements into the venv.
|
|
251
|
+
local req="$root/mcp/requirements.txt"
|
|
252
|
+
if [ ! -f "$req" ]; then
|
|
253
|
+
printf '%smcp/requirements.txt not found at %s.%s\n' "$_ML_RED" "$req" "$_ML_NC" >&2
|
|
254
|
+
return 2
|
|
255
|
+
fi
|
|
256
|
+
printf 'Installing MCP dependencies (%s/bin/pip install -r %s) ...\n' "$venv" "$req"
|
|
257
|
+
local code=0
|
|
258
|
+
"$venv/bin/pip" install -r "$req" || code=$?
|
|
259
|
+
if [ "$code" -ne 0 ]; then
|
|
260
|
+
printf '%sInstall failed (pip exited %s).%s You can retry manually:\n' "$_ML_RED" "$code" "$_ML_NC" >&2
|
|
261
|
+
_ml_print_manual "$root" "$venv"
|
|
262
|
+
return 2
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
# 9. Verify, then exec the server using the venv python (critical: the
|
|
266
|
+
# site-packages walk in server.py only finds the SDK if we run the
|
|
267
|
+
# venv's interpreter, not the ambient python3). PYTHONPATH=$root keeps the
|
|
268
|
+
# user's cwd intact for .loki resolution; see _ml_sdk_importable.
|
|
269
|
+
if ! _ml_sdk_importable "$venv_py" "$root"; then
|
|
270
|
+
printf '%sDependencies installed but the MCP SDK still is not importable.%s\n' "$_ML_RED" "$_ML_NC" >&2
|
|
271
|
+
_ml_print_manual "$root" "$venv"
|
|
272
|
+
return 2
|
|
273
|
+
fi
|
|
274
|
+
printf "%sMCP dependencies ready. Launching server over stdio ...%s\n" "$_ML_BOLD" "$_ML_NC" >&2
|
|
275
|
+
exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Executed directly (tests, manual): run the dispatcher.
|
|
279
|
+
# When sourced by autonomy/loki, this block does not run.
|
|
280
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
281
|
+
mcp_launch_main "$@"
|
|
282
|
+
fi
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# provider-offer.sh -- shared, self-contained provider install offer (v7.29.0).
|
|
3
|
+
#
|
|
4
|
+
# Single source of truth for "no AI provider CLI found" handling, used by BOTH
|
|
5
|
+
# the bash CLI (autonomy/loki, sourced) and the Bun-routed doctor
|
|
6
|
+
# (loki-ts/src/commands/doctor.ts, via child_process). Parity is by
|
|
7
|
+
# construction: there is exactly one prompt + npm install + login handoff
|
|
8
|
+
# implementation, and both routes call it.
|
|
9
|
+
#
|
|
10
|
+
# Self-containment contract (load-bearing for parity): this file depends ONLY
|
|
11
|
+
# on bash builtins + npm/claude on PATH. It defines its own colors and never
|
|
12
|
+
# reads $RED/$NC or any other variable owned by autonomy/loki, because when
|
|
13
|
+
# doctor.ts spawns it standalone those variables are unset. If this file ever
|
|
14
|
+
# starts depending on loki's environment, the bash-route and Bun-route bytes
|
|
15
|
+
# diverge and the bun-parity matrix breaks.
|
|
16
|
+
#
|
|
17
|
+
# Security posture (design 1.7): the ONLY command ever executed on the user's
|
|
18
|
+
# behalf is `npm install -g @anthropic-ai/claude-code`, only after explicit
|
|
19
|
+
# consent, with the exact command printed first. No sudo. No curl-pipe-bash.
|
|
20
|
+
# Non-interactive / CI contexts never run an install.
|
|
21
|
+
|
|
22
|
+
# Guard against double-source (loki may source this more than once via reloads).
|
|
23
|
+
if [ -n "${_LOKI_PROVIDER_OFFER_SOURCED:-}" ]; then
|
|
24
|
+
return 0 2>/dev/null || true
|
|
25
|
+
fi
|
|
26
|
+
_LOKI_PROVIDER_OFFER_SOURCED=1
|
|
27
|
+
|
|
28
|
+
# --- Self-contained colors (honor NO_COLOR; no dependency on loki) ----------
|
|
29
|
+
if [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then
|
|
30
|
+
_PO_RED=''; _PO_YELLOW=''; _PO_BOLD=''; _PO_DIM=''; _PO_NC=''
|
|
31
|
+
else
|
|
32
|
+
_PO_RED=$'\033[0;31m'
|
|
33
|
+
_PO_YELLOW=$'\033[1;33m'
|
|
34
|
+
_PO_BOLD=$'\033[1m'
|
|
35
|
+
_PO_NC=$'\033[0m'
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# The one canonical install command. Quoted everywhere; never re-derived.
|
|
39
|
+
_PO_INSTALL_CMD="npm install -g @anthropic-ai/claude-code"
|
|
40
|
+
|
|
41
|
+
# detect_any_provider: true (0) if any supported provider CLI is on PATH.
|
|
42
|
+
# Extracted verbatim from the loki doctor detection loop (design 1.2).
|
|
43
|
+
detect_any_provider() {
|
|
44
|
+
local _dp
|
|
45
|
+
for _dp in claude codex cline aider; do
|
|
46
|
+
command -v "$_dp" >/dev/null 2>&1 && return 0
|
|
47
|
+
done
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# _po_assume_yes: true when the user has opted into unattended confirmation.
|
|
52
|
+
# Honors --yes (LOKI_AUTO_CONFIRM, set by loki:1013) and LOKI_ASSUME_YES.
|
|
53
|
+
_po_assume_yes() {
|
|
54
|
+
[ "${LOKI_ASSUME_YES:-}" = "1" ] && return 0
|
|
55
|
+
[ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 0
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# _po_non_interactive: true when we must NEVER prompt (non-TTY or CI).
|
|
60
|
+
# Mirrors cmd_welcome_maybe_firstrun (loki:4286) and maybe_show_auto_plan.
|
|
61
|
+
_po_non_interactive() {
|
|
62
|
+
[ ! -t 1 ] && return 0
|
|
63
|
+
[ ! -t 0 ] && return 0
|
|
64
|
+
[ -n "${CI:-}" ] && return 0
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# _po_run_login: offer (or auto-accept) the claude auth login handoff after a
|
|
69
|
+
# successful install. Inherited stdio; Loki never handles credentials.
|
|
70
|
+
_po_run_login() {
|
|
71
|
+
# claude must actually be on PATH for login to make sense.
|
|
72
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
73
|
+
printf "%sInstalled, but 'claude' is not on your PATH yet. You may need to restart your shell or add npm's global bin to PATH (npm config get prefix). Run 'loki doctor' to recheck.%s\n" "$_PO_YELLOW" "$_PO_NC"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
local do_login=""
|
|
78
|
+
if _po_assume_yes; then
|
|
79
|
+
do_login="y"
|
|
80
|
+
else
|
|
81
|
+
printf 'Claude Code installed.\n'
|
|
82
|
+
printf '\n'
|
|
83
|
+
printf 'You still need to authenticate. Run the login flow now? [Y/n] '
|
|
84
|
+
read -r do_login || do_login="n"
|
|
85
|
+
fi
|
|
86
|
+
case "$do_login" in
|
|
87
|
+
""|y|Y|yes|YES)
|
|
88
|
+
if claude auth login; then
|
|
89
|
+
# Do not trust the exit code alone: verify the session is
|
|
90
|
+
# actually authenticated before claiming readiness (council
|
|
91
|
+
# HIGH: the old path could falsely report success).
|
|
92
|
+
if claude auth status 2>/dev/null | grep -q '"loggedIn"[[:space:]]*:[[:space:]]*true'; then
|
|
93
|
+
printf "%sProvider ready. Run 'loki doctor' to confirm, or 'loki quickstart' to build.%s\n" "$_PO_BOLD" "$_PO_NC"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
printf "Login finished but authentication could not be confirmed. Run 'claude auth status' to check, then 'loki doctor'.\n"
|
|
97
|
+
return 0
|
|
98
|
+
fi
|
|
99
|
+
printf "Login not completed. Run 'claude auth login' when ready, then 'loki doctor'.\n"
|
|
100
|
+
return 0
|
|
101
|
+
;;
|
|
102
|
+
*)
|
|
103
|
+
printf "Login not completed. Run 'claude auth login' when ready, then 'loki doctor'.\n"
|
|
104
|
+
return 0
|
|
105
|
+
;;
|
|
106
|
+
esac
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# _po_do_install: run the one consented command, print it first, handle result.
|
|
110
|
+
# Returns 0 on success, non-zero on failure (caller decides exit behavior).
|
|
111
|
+
_po_do_install() {
|
|
112
|
+
printf 'Installing Claude Code (%s) ...\n' "$_PO_INSTALL_CMD"
|
|
113
|
+
# The exact, fixed argv. No interpolation, no extra flags. (design 1.7)
|
|
114
|
+
# Capture npm's exit code directly (not via `if`, whose statement status is
|
|
115
|
+
# 0 when the condition is false with no else, masking the real npm code).
|
|
116
|
+
local code=0
|
|
117
|
+
npm install -g @anthropic-ai/claude-code || code=$?
|
|
118
|
+
if [ "$code" -eq 0 ]; then
|
|
119
|
+
printf '\n'
|
|
120
|
+
_po_run_login
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
printf '%sInstall failed (npm exited %s). You can retry manually:%s\n' "$_PO_RED" "$code" "$_PO_NC"
|
|
124
|
+
printf ' %s\n' "$_PO_INSTALL_CMD"
|
|
125
|
+
printf 'If this is a permissions error, see https://docs.npmjs.com/resolving-eacces-permissions-errors\n'
|
|
126
|
+
return "$code"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# offer_provider_install <mode>
|
|
130
|
+
# mode = "report" -> doctor: append the offer on a TTY; on non-TTY/CI do
|
|
131
|
+
# NOTHING (doctor already printed the FAIL + install line,
|
|
132
|
+
# and we must keep non-TTY/json bytes identical for parity).
|
|
133
|
+
# Never exits the process.
|
|
134
|
+
# mode = "gate" -> start/demo/quick pre-flight: on non-TTY/CI print the
|
|
135
|
+
# honest one-liner to stderr and return 2. On a TTY, prompt;
|
|
136
|
+
# on decline return 2. On accept install + login.
|
|
137
|
+
#
|
|
138
|
+
# Honors:
|
|
139
|
+
# LOKI_NO_INSTALL_OFFER=1 -> never prompt; print manual command (1.4)
|
|
140
|
+
# --yes / LOKI_ASSUME_YES -> auto-accept install + login (1.4)
|
|
141
|
+
offer_provider_install() {
|
|
142
|
+
local mode="${1:-gate}"
|
|
143
|
+
|
|
144
|
+
# Opt-out: never offer, just surface the manual command.
|
|
145
|
+
if [ "${LOKI_NO_INSTALL_OFFER:-}" = "1" ]; then
|
|
146
|
+
if [ "$mode" = "gate" ]; then
|
|
147
|
+
printf 'No AI provider CLI found. Install one when ready:\n' >&2
|
|
148
|
+
printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD" >&2
|
|
149
|
+
return 2
|
|
150
|
+
fi
|
|
151
|
+
printf '\n'
|
|
152
|
+
printf 'Install a provider when ready:\n'
|
|
153
|
+
printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD"
|
|
154
|
+
printf ' Other supported providers: codex, cline, aider.\n'
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Non-interactive / CI: NEVER prompt, NEVER install.
|
|
159
|
+
#
|
|
160
|
+
# gate (start/demo/quick): print the honest one-liner to stderr and return 2
|
|
161
|
+
# so the caller exits with an actionable message before any spend.
|
|
162
|
+
# report (doctor): stay SILENT. doctor has already printed the FAIL line and
|
|
163
|
+
# the install command on stdout, so no information is lost. Silence here is
|
|
164
|
+
# load-bearing for parity: doctor.ts gates its child_process bridge on
|
|
165
|
+
# process.stdout.isTTY, so on a non-TTY/CI run the Bun route emits nothing
|
|
166
|
+
# extra. If report-mode printed a stderr line, the bash route would diverge
|
|
167
|
+
# from Bun in exactly the no-provider/non-TTY case the bun-parity matrix
|
|
168
|
+
# captures (2>&1) on CI runners, which have no provider installed.
|
|
169
|
+
if _po_non_interactive; then
|
|
170
|
+
if [ "$mode" = "gate" ]; then
|
|
171
|
+
printf 'No AI provider CLI found; cannot prompt to install in a non-interactive shell. Run: %s\n' "$_PO_INSTALL_CMD" >&2
|
|
172
|
+
return 2
|
|
173
|
+
fi
|
|
174
|
+
return 0
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# npm missing: degraded path, never attempt a non-npm install.
|
|
178
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
179
|
+
printf '\n'
|
|
180
|
+
printf '%sNo AI provider CLI was found, and npm is not installed either, so Loki%s\n' "$_PO_BOLD" "$_PO_NC"
|
|
181
|
+
printf 'cannot install one for you.\n'
|
|
182
|
+
printf '\n'
|
|
183
|
+
printf 'Install Node.js + npm first (https://nodejs.org), then run:\n'
|
|
184
|
+
printf ' %s\n' "$_PO_INSTALL_CMD"
|
|
185
|
+
printf ' claude auth login\n'
|
|
186
|
+
printf '\n'
|
|
187
|
+
printf "Already have a provider via another method? Make sure 'claude' (or codex,\n"
|
|
188
|
+
printf "cline, aider) is on your PATH, then run 'loki doctor'.\n"
|
|
189
|
+
[ "$mode" = "gate" ] && return 2
|
|
190
|
+
return 0
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# TTY, npm present: the interactive offer.
|
|
194
|
+
printf '\n'
|
|
195
|
+
printf 'No AI provider CLI was found. Loki needs one agent CLI to run a build.\n'
|
|
196
|
+
printf '\n'
|
|
197
|
+
printf 'Claude Code is the recommended provider (full feature support).\n'
|
|
198
|
+
printf ' Install: %s\n' "$_PO_INSTALL_CMD"
|
|
199
|
+
printf ' Then: claude auth login\n'
|
|
200
|
+
printf '\n'
|
|
201
|
+
|
|
202
|
+
local answer=""
|
|
203
|
+
if _po_assume_yes; then
|
|
204
|
+
answer="y"
|
|
205
|
+
else
|
|
206
|
+
printf 'Install Claude Code now? [Y/n] '
|
|
207
|
+
read -r answer || answer="n"
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
case "$answer" in
|
|
211
|
+
""|y|Y|yes|YES)
|
|
212
|
+
if _po_do_install; then
|
|
213
|
+
return 0
|
|
214
|
+
fi
|
|
215
|
+
# Install failed: honest failure already printed by _po_do_install.
|
|
216
|
+
[ "$mode" = "gate" ] && return 2
|
|
217
|
+
return 1
|
|
218
|
+
;;
|
|
219
|
+
*)
|
|
220
|
+
printf 'Skipped. Install a provider when ready:\n'
|
|
221
|
+
printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD"
|
|
222
|
+
printf 'Other supported providers: codex, cline, aider.\n'
|
|
223
|
+
[ "$mode" = "gate" ] && return 2
|
|
224
|
+
return 0
|
|
225
|
+
;;
|
|
226
|
+
esac
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# provider_offer_gate: convenience wrapper for the start/demo/quick pre-flight.
|
|
230
|
+
# Returns 0 if a provider is present (or one was just installed); returns 2 to
|
|
231
|
+
# signal the caller should `exit 2` (no provider, declined or non-interactive).
|
|
232
|
+
provider_offer_gate() {
|
|
233
|
+
detect_any_provider && return 0
|
|
234
|
+
offer_provider_install gate || return 2
|
|
235
|
+
# After an accepted install, re-detect; if still absent, fail the gate.
|
|
236
|
+
detect_any_provider && return 0
|
|
237
|
+
return 2
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Executed directly (doctor.ts child_process bridge, or manual): run the offer.
|
|
241
|
+
# When sourced by autonomy/loki, this block does not run.
|
|
242
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
243
|
+
case "${1:-report}" in
|
|
244
|
+
offer|report) offer_provider_install report ;;
|
|
245
|
+
gate) offer_provider_install gate ;;
|
|
246
|
+
detect) detect_any_provider ;;
|
|
247
|
+
*) offer_provider_install report ;;
|
|
248
|
+
esac
|
|
249
|
+
fi
|