loki-mode 7.7.28 → 7.7.29
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/loki +132 -36
- package/autonomy/run.sh +41 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +65 -0
- package/dashboard/static/index.html +67 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +178 -159
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.7.
|
|
6
|
+
# Loki Mode v7.7.29
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.7.
|
|
384
|
+
**v7.7.29 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.29
|
package/autonomy/loki
CHANGED
|
@@ -1593,12 +1593,19 @@ cmd_start() {
|
|
|
1593
1593
|
exit 1
|
|
1594
1594
|
fi
|
|
1595
1595
|
|
|
1596
|
-
# --api flag: start the dashboard API server
|
|
1596
|
+
# --api flag: start the dashboard API server before the build.
|
|
1597
|
+
# cmd_dashboard_start already daemonizes the server (nohup ... &) and runs
|
|
1598
|
+
# its own readiness probe, so run it in a contained SUBSHELL (not a
|
|
1599
|
+
# double-background) and surface the outcome. The old
|
|
1600
|
+
# `cmd_dashboard_start 2>/dev/null &` swallowed all output AND double-
|
|
1601
|
+
# backgrounded the already-daemonized server, so a port-in-use or startup
|
|
1602
|
+
# failure was invisible and the user got no dashboard and no error. The
|
|
1603
|
+
# subshell also contains cmd_dashboard_start's internal `exit 1` so a
|
|
1604
|
+
# dashboard failure can never abort the build.
|
|
1597
1605
|
if [ "${LOKI_START_API:-false}" = "true" ]; then
|
|
1598
1606
|
local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
1599
1607
|
echo -e "${GREEN}Starting dashboard API on port $dash_port...${NC}"
|
|
1600
|
-
cmd_dashboard_start
|
|
1601
|
-
sleep 2
|
|
1608
|
+
( cmd_dashboard_start ) || echo -e "${YELLOW}Dashboard API did not start (see message above); continuing the build without it.${NC}"
|
|
1602
1609
|
fi
|
|
1603
1610
|
|
|
1604
1611
|
# Phase F (v7.5.23): cross-project context discovery. Walks ONE parent
|
|
@@ -1896,17 +1903,23 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
1896
1903
|
# Clean up control files
|
|
1897
1904
|
rm -f "$LOKI_DIR/STOP" "$LOKI_DIR/PAUSE" "$LOKI_DIR/PAUSED.md" 2>/dev/null
|
|
1898
1905
|
|
|
1899
|
-
# Kill dashboard if running
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1906
|
+
# Kill dashboard if running. Check BOTH the project-local in-build
|
|
1907
|
+
# dashboard (.loki/dashboard) and the standalone one
|
|
1908
|
+
# (~/.loki/dashboard) so cleanup never leaves an orphan regardless of
|
|
1909
|
+
# which path started it.
|
|
1910
|
+
local _dash_pidf
|
|
1911
|
+
for _dash_pidf in "$LOKI_DIR/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
|
|
1912
|
+
if [ -f "$_dash_pidf" ]; then
|
|
1913
|
+
local dash_pid
|
|
1914
|
+
dash_pid=$(cat "$_dash_pidf" 2>/dev/null)
|
|
1915
|
+
if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
|
|
1916
|
+
kill "$dash_pid" 2>/dev/null || true
|
|
1917
|
+
sleep 0.5
|
|
1918
|
+
kill -9 "$dash_pid" 2>/dev/null || true
|
|
1919
|
+
fi
|
|
1920
|
+
rm -f "$_dash_pidf"
|
|
1907
1921
|
fi
|
|
1908
|
-
|
|
1909
|
-
fi
|
|
1922
|
+
done
|
|
1910
1923
|
|
|
1911
1924
|
# Kill any remaining registered processes (2s graceful window matches run.sh)
|
|
1912
1925
|
if [ -d "$LOKI_DIR/pids" ]; then
|
|
@@ -2536,15 +2549,33 @@ if os.path.isfile(session_file):
|
|
|
2536
2549
|
else:
|
|
2537
2550
|
result['elapsed_time'] = 0
|
|
2538
2551
|
|
|
2539
|
-
# Dashboard URL
|
|
2540
|
-
|
|
2552
|
+
# Dashboard URL. Check both the project-local in-build dashboard and the
|
|
2553
|
+
# standalone dashboard (~/.loki/dashboard) and honor saved scheme/host/port
|
|
2554
|
+
# side-files, so --json reports the right URL regardless of which started it.
|
|
2555
|
+
_dash_candidates = [
|
|
2556
|
+
os.path.join(loki_dir, 'dashboard', 'dashboard.pid'),
|
|
2557
|
+
os.path.expanduser(os.path.join('~', '.loki', 'dashboard', 'dashboard.pid')),
|
|
2558
|
+
]
|
|
2559
|
+
dashboard_pid_file = next((p for p in _dash_candidates if os.path.isfile(p)), _dash_candidates[0])
|
|
2541
2560
|
dashboard_url = None
|
|
2542
2561
|
if os.path.isfile(dashboard_pid_file):
|
|
2543
2562
|
try:
|
|
2544
2563
|
with open(dashboard_pid_file) as f:
|
|
2545
2564
|
dpid = int(f.read().strip())
|
|
2546
2565
|
os.kill(dpid, 0)
|
|
2547
|
-
|
|
2566
|
+
_dd = os.path.dirname(dashboard_pid_file)
|
|
2567
|
+
def _side(name, default):
|
|
2568
|
+
p = os.path.join(_dd, name)
|
|
2569
|
+
try:
|
|
2570
|
+
return open(p).read().strip() if os.path.isfile(p) else default
|
|
2571
|
+
except OSError:
|
|
2572
|
+
return default
|
|
2573
|
+
_scheme = _side('scheme', 'http')
|
|
2574
|
+
_host = _side('host', '127.0.0.1')
|
|
2575
|
+
_port = _side('port', str(dashboard_port))
|
|
2576
|
+
if _host == '0.0.0.0':
|
|
2577
|
+
_host = '127.0.0.1'
|
|
2578
|
+
dashboard_url = _scheme + '://' + _host + ':' + _port + '/'
|
|
2548
2579
|
except (ProcessLookupError, PermissionError, ValueError, Exception):
|
|
2549
2580
|
pass
|
|
2550
2581
|
result['dashboard_url'] = dashboard_url
|
|
@@ -3296,11 +3327,24 @@ cmd_provider_models() {
|
|
|
3296
3327
|
done
|
|
3297
3328
|
}
|
|
3298
3329
|
|
|
3299
|
-
#
|
|
3300
|
-
|
|
3330
|
+
# Standalone dashboard server management (`loki dashboard`).
|
|
3331
|
+
# State lives at a FIXED, machine-global ~/.loki/dashboard so that
|
|
3332
|
+
# stop|status|open find the running server from ANY working directory.
|
|
3333
|
+
# (The relative ${LOKI_DIR}/dashboard path made these commands silently
|
|
3334
|
+
# fail from a different cwd than `start`, orphaning the server.) The
|
|
3335
|
+
# separate in-build dashboard started by run.sh during `loki start` stays
|
|
3336
|
+
# project-local and is unaffected by this constant.
|
|
3337
|
+
DASHBOARD_PID_DIR="${HOME}/.loki/dashboard"
|
|
3301
3338
|
DASHBOARD_PID_FILE="${DASHBOARD_PID_DIR}/dashboard.pid"
|
|
3302
3339
|
DASHBOARD_DEFAULT_PORT=57374
|
|
3303
|
-
|
|
3340
|
+
# Default bind host: 0.0.0.0 inside a container (so a -p port map reaches
|
|
3341
|
+
# the server; 127.0.0.1 would be unreachable from the host), else loopback
|
|
3342
|
+
# for host safety. Detect Docker via /.dockerenv or LOKI_SANDBOX_MODE.
|
|
3343
|
+
if [ -f /.dockerenv ] || [ "${LOKI_SANDBOX_MODE:-}" = "true" ] || [ -n "${LOKI_IN_CONTAINER:-}" ]; then
|
|
3344
|
+
DASHBOARD_DEFAULT_HOST="0.0.0.0"
|
|
3345
|
+
else
|
|
3346
|
+
DASHBOARD_DEFAULT_HOST="127.0.0.1"
|
|
3347
|
+
fi
|
|
3304
3348
|
|
|
3305
3349
|
cmd_dashboard() {
|
|
3306
3350
|
local subcommand="${1:-}"
|
|
@@ -3546,12 +3590,19 @@ cmd_dashboard_start() {
|
|
|
3546
3590
|
echo "$host" > "${DASHBOARD_PID_DIR}/host"
|
|
3547
3591
|
echo "$url_scheme" > "${DASHBOARD_PID_DIR}/scheme"
|
|
3548
3592
|
|
|
3549
|
-
# Wait for dashboard
|
|
3593
|
+
# Wait for the dashboard to become ready (up to 10 seconds). Probe the
|
|
3594
|
+
# UNAUTHENTICATED /health endpoint over the ACTUAL scheme: /api/status
|
|
3595
|
+
# 401s under LOKI_ENTERPRISE_AUTH, and the scheme is https when TLS is on,
|
|
3596
|
+
# so the old hardcoded http://.../api/status probe always failed against a
|
|
3597
|
+
# healthy TLS or auth-enabled server. Use a loopback-reachable host when
|
|
3598
|
+
# bound to 0.0.0.0, and -k to tolerate self-signed certs.
|
|
3599
|
+
local health_host="$host"; [ "$health_host" = "0.0.0.0" ] && health_host="127.0.0.1"
|
|
3600
|
+
local health_curl_opts="-sf"; [ "$url_scheme" = "https" ] && health_curl_opts="-sfk"
|
|
3550
3601
|
local health_retries=20
|
|
3551
3602
|
local health_interval=0.5
|
|
3552
3603
|
local health_ok=false
|
|
3553
3604
|
while [[ $health_retries -gt 0 ]]; do
|
|
3554
|
-
if curl
|
|
3605
|
+
if curl $health_curl_opts "${url_scheme}://${health_host}:${port}/health" >/dev/null 2>&1; then
|
|
3555
3606
|
health_ok=true
|
|
3556
3607
|
break
|
|
3557
3608
|
fi
|
|
@@ -7802,11 +7853,37 @@ cmd_logs() {
|
|
|
7802
7853
|
fi
|
|
7803
7854
|
}
|
|
7804
7855
|
|
|
7805
|
-
# API server management (delegates to unified FastAPI dashboard server)
|
|
7856
|
+
# API server management (delegates to unified FastAPI dashboard server).
|
|
7857
|
+
# Shares the SAME machine-global PID/control dir as `loki dashboard`
|
|
7858
|
+
# ($DASHBOARD_PID_DIR = ~/.loki/dashboard) so the two commands never fight
|
|
7859
|
+
# over the port and so stop/status/open are consistent across both. Parses
|
|
7860
|
+
# --host/--port, guards a busy port, computes the TLS scheme, and persists
|
|
7861
|
+
# host/port/scheme side-files like cmd_dashboard_start does.
|
|
7806
7862
|
cmd_api() {
|
|
7807
7863
|
local subcommand="${1:-help}"
|
|
7808
|
-
|
|
7809
|
-
local
|
|
7864
|
+
shift || true
|
|
7865
|
+
local port="${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}"
|
|
7866
|
+
local host="${LOKI_DASHBOARD_HOST:-$DASHBOARD_DEFAULT_HOST}"
|
|
7867
|
+
local pid_file="$DASHBOARD_PID_FILE"
|
|
7868
|
+
|
|
7869
|
+
# Parse --host/--port (the old code ignored them, so `loki serve --port X`
|
|
7870
|
+
# silently bound 57374 and `--host 0.0.0.0` was dropped).
|
|
7871
|
+
while [ $# -gt 0 ]; do
|
|
7872
|
+
case "$1" in
|
|
7873
|
+
--port) port="${2:-$port}"; shift 2 ;;
|
|
7874
|
+
--port=*) port="${1#*=}"; shift ;;
|
|
7875
|
+
--host) host="${2:-$host}"; shift 2 ;;
|
|
7876
|
+
--host=*) host="${1#*=}"; shift ;;
|
|
7877
|
+
*) shift ;;
|
|
7878
|
+
esac
|
|
7879
|
+
done
|
|
7880
|
+
|
|
7881
|
+
local scheme="http"
|
|
7882
|
+
if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
|
|
7883
|
+
scheme="https"
|
|
7884
|
+
fi
|
|
7885
|
+
# Loopback-reachable host for printed URLs when bound to 0.0.0.0.
|
|
7886
|
+
local url_host="$host"; [ "$url_host" = "0.0.0.0" ] && url_host="127.0.0.1"
|
|
7810
7887
|
|
|
7811
7888
|
case "$subcommand" in
|
|
7812
7889
|
start)
|
|
@@ -7818,12 +7895,20 @@ cmd_api() {
|
|
|
7818
7895
|
|
|
7819
7896
|
# Check if already running
|
|
7820
7897
|
if [ -f "$pid_file" ]; then
|
|
7821
|
-
local existing_pid=$(cat "$pid_file")
|
|
7822
|
-
if kill -0 "$existing_pid" 2>/dev/null; then
|
|
7898
|
+
local existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
7899
|
+
if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
7823
7900
|
echo -e "${YELLOW}Dashboard server already running (PID: $existing_pid)${NC}"
|
|
7824
|
-
echo "URL:
|
|
7901
|
+
echo "URL: ${scheme}://${url_host}:$port"
|
|
7825
7902
|
exit 0
|
|
7826
7903
|
fi
|
|
7904
|
+
rm -f "$pid_file"
|
|
7905
|
+
fi
|
|
7906
|
+
|
|
7907
|
+
# Refuse a busy port instead of writing a bogus PID.
|
|
7908
|
+
if command -v lsof &> /dev/null && lsof -i ":$port" &> /dev/null; then
|
|
7909
|
+
echo -e "${RED}Error: Port $port is already in use${NC}"
|
|
7910
|
+
echo "Stop the other server or set LOKI_DASHBOARD_PORT / --port to a free port."
|
|
7911
|
+
exit 1
|
|
7827
7912
|
fi
|
|
7828
7913
|
|
|
7829
7914
|
# Ensure dashboard Python dependencies
|
|
@@ -7833,20 +7918,24 @@ cmd_api() {
|
|
|
7833
7918
|
fi
|
|
7834
7919
|
local api_python="$DASHBOARD_PYTHON"
|
|
7835
7920
|
|
|
7836
|
-
# Start server
|
|
7837
|
-
mkdir -p "$LOKI_DIR/logs" "$
|
|
7838
|
-
local host="${LOKI_DASHBOARD_HOST:-127.0.0.1}"
|
|
7921
|
+
# Start server. Persist host/port/scheme so status/open are accurate.
|
|
7922
|
+
mkdir -p "$LOKI_DIR/logs" "$DASHBOARD_PID_DIR"
|
|
7839
7923
|
local uvicorn_args=("--host" "$host" "--port" "$port")
|
|
7840
|
-
if [
|
|
7924
|
+
if [ "$scheme" = "https" ]; then
|
|
7841
7925
|
uvicorn_args+=("--ssl-certfile" "${LOKI_TLS_CERT}" "--ssl-keyfile" "${LOKI_TLS_KEY}")
|
|
7842
7926
|
fi
|
|
7843
|
-
LOKI_DIR="$LOKI_DIR"
|
|
7927
|
+
LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" PYTHONPATH="$SKILL_DIR" \
|
|
7928
|
+
LOKI_DASHBOARD_HOST="$host" LOKI_DASHBOARD_PORT="$port" \
|
|
7929
|
+
nohup "$api_python" -m uvicorn dashboard.server:app "${uvicorn_args[@]}" > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
7844
7930
|
local new_pid=$!
|
|
7845
7931
|
echo "$new_pid" > "$pid_file"
|
|
7932
|
+
echo "$port" > "${DASHBOARD_PID_DIR}/port"
|
|
7933
|
+
echo "$host" > "${DASHBOARD_PID_DIR}/host"
|
|
7934
|
+
echo "$scheme" > "${DASHBOARD_PID_DIR}/scheme"
|
|
7846
7935
|
|
|
7847
7936
|
echo -e "${GREEN}Dashboard server started${NC}"
|
|
7848
7937
|
echo " PID: $new_pid"
|
|
7849
|
-
echo " URL:
|
|
7938
|
+
echo " URL: ${scheme}://${url_host}:$port"
|
|
7850
7939
|
echo " Logs: $LOKI_DIR/logs/api.log"
|
|
7851
7940
|
;;
|
|
7852
7941
|
|
|
@@ -7870,15 +7959,22 @@ cmd_api() {
|
|
|
7870
7959
|
if [ -f "$pid_file" ]; then
|
|
7871
7960
|
local pid=$(cat "$pid_file")
|
|
7872
7961
|
if kill -0 "$pid" 2>/dev/null; then
|
|
7962
|
+
# Prefer persisted side-files for the real bind.
|
|
7963
|
+
local s_scheme="$scheme" s_host="$url_host" s_port="$port"
|
|
7964
|
+
[ -f "${DASHBOARD_PID_DIR}/scheme" ] && s_scheme="$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)"
|
|
7965
|
+
[ -f "${DASHBOARD_PID_DIR}/host" ] && s_host="$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)"
|
|
7966
|
+
[ -f "${DASHBOARD_PID_DIR}/port" ] && s_port="$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)"
|
|
7967
|
+
[ "$s_host" = "0.0.0.0" ] && s_host="127.0.0.1"
|
|
7873
7968
|
echo -e "${GREEN}Dashboard server running${NC}"
|
|
7874
7969
|
echo " PID: $pid"
|
|
7875
|
-
echo " URL:
|
|
7970
|
+
echo " URL: ${s_scheme}://${s_host}:${s_port}"
|
|
7876
7971
|
|
|
7877
|
-
# Try to fetch status
|
|
7972
|
+
# Try to fetch status (best-effort; may 401 under auth)
|
|
7878
7973
|
if command -v curl &> /dev/null; then
|
|
7879
7974
|
echo ""
|
|
7880
7975
|
echo -e "${CYAN}Status:${NC}"
|
|
7881
|
-
|
|
7976
|
+
local _k=""; [ "$s_scheme" = "https" ] && _k="-k"
|
|
7977
|
+
curl -s $_k "${s_scheme}://${s_host}:${s_port}/api/status" 2>/dev/null | jq . 2>/dev/null || true
|
|
7882
7978
|
fi
|
|
7883
7979
|
else
|
|
7884
7980
|
echo -e "${YELLOW}Dashboard server not running (stale PID file)${NC}"
|
package/autonomy/run.sh
CHANGED
|
@@ -752,6 +752,47 @@ TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}"
|
|
|
752
752
|
PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false}
|
|
753
753
|
AUTO_MERGE=${LOKI_AUTO_MERGE:-true}
|
|
754
754
|
|
|
755
|
+
# Multi-project registry (v7.7.29): register this running project in the
|
|
756
|
+
# machine-global registry (~/.loki/dashboard/projects.json) so the dashboard
|
|
757
|
+
# can list and switch between projects running in different folders. Records
|
|
758
|
+
# the absolute path, pid, port, and status. Fully non-blocking and
|
|
759
|
+
# failure-swallowed: registry problems must never affect a build. Marked
|
|
760
|
+
# inactive again on exit via the trap below.
|
|
761
|
+
loki_register_running_project() {
|
|
762
|
+
local _status="${1:-running}"
|
|
763
|
+
[ -n "${LOKI_SKIP_PROJECT_REGISTRY:-}" ] && return 0
|
|
764
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
765
|
+
local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
|
|
766
|
+
LOKI_REG_TARGET="$TARGET_DIR" LOKI_REG_SKILL="$_skill" \
|
|
767
|
+
LOKI_REG_PID="$$" LOKI_REG_PORT="${LOKI_DASHBOARD_PORT:-57374}" \
|
|
768
|
+
LOKI_REG_STATUS="$_status" \
|
|
769
|
+
python3 - <<'PYREG' >/dev/null 2>&1 || true
|
|
770
|
+
import os, sys
|
|
771
|
+
sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
|
|
772
|
+
try:
|
|
773
|
+
from dashboard import registry
|
|
774
|
+
target = os.path.abspath(os.environ["LOKI_REG_TARGET"])
|
|
775
|
+
entry = registry.register_project(target)
|
|
776
|
+
# Enrich with runtime fields the dashboard switcher uses.
|
|
777
|
+
reg = registry._load_registry()
|
|
778
|
+
pid = entry.get("id") or registry._generate_project_id(target)
|
|
779
|
+
if pid in reg.get("projects", {}):
|
|
780
|
+
reg["projects"][pid]["pid"] = int(os.environ.get("LOKI_REG_PID", "0") or 0)
|
|
781
|
+
reg["projects"][pid]["port"] = int(os.environ.get("LOKI_REG_PORT", "57374") or 57374)
|
|
782
|
+
reg["projects"][pid]["status"] = os.environ.get("LOKI_REG_STATUS", "running")
|
|
783
|
+
registry._save_registry(reg)
|
|
784
|
+
except Exception:
|
|
785
|
+
pass
|
|
786
|
+
PYREG
|
|
787
|
+
}
|
|
788
|
+
# Register as running now. We deliberately do NOT install an EXIT trap to
|
|
789
|
+
# flip it to idle: a top-level EXIT trap here would be clobbered by the
|
|
790
|
+
# lock-release EXIT trap installed later in the main path (and could
|
|
791
|
+
# interfere with it). Instead the dashboard determines live vs stale by
|
|
792
|
+
# checking whether the recorded pid is still alive (registry stores pid),
|
|
793
|
+
# which is robust even on hard kills where a trap would never fire.
|
|
794
|
+
loki_register_running_project running
|
|
795
|
+
|
|
755
796
|
# Complexity Tiers (Auto-Claude pattern)
|
|
756
797
|
# auto = detect from PRD/codebase, simple = 3 phases, standard = 6 phases, complex = 8 phases
|
|
757
798
|
COMPLEXITY_TIER=${LOKI_COMPLEXITY:-auto}
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -2140,6 +2140,71 @@ async def clear_focus():
|
|
|
2140
2140
|
return {"project_dir": None, "loki_dir": str(_get_loki_dir())}
|
|
2141
2141
|
|
|
2142
2142
|
|
|
2143
|
+
@app.get("/api/running-projects")
|
|
2144
|
+
async def list_running_projects():
|
|
2145
|
+
"""List registered projects enriched with live status for the dashboard
|
|
2146
|
+
project switcher (v7.7.29 multi-project support).
|
|
2147
|
+
|
|
2148
|
+
NOTE: deliberately NOT under /api/projects/* because /api/projects/{id}
|
|
2149
|
+
(int path param) would shadow a /api/projects/running literal and 422.
|
|
2150
|
+
|
|
2151
|
+
Returns every registered project (from ~/.loki/dashboard/projects.json,
|
|
2152
|
+
populated by `loki start`), each annotated with:
|
|
2153
|
+
- running: whether the recorded orchestrator pid is still alive
|
|
2154
|
+
- is_active: whether it is the currently focused project
|
|
2155
|
+
Live-vs-stale is derived from pid liveness, which is robust even when a
|
|
2156
|
+
session is hard-killed (no exit hook fires). Never raises: registry
|
|
2157
|
+
problems degrade to an empty list.
|
|
2158
|
+
"""
|
|
2159
|
+
out = []
|
|
2160
|
+
try:
|
|
2161
|
+
projects = registry.list_projects(include_inactive=True)
|
|
2162
|
+
except Exception:
|
|
2163
|
+
projects = []
|
|
2164
|
+
active = _active_project_dir
|
|
2165
|
+
for p in projects:
|
|
2166
|
+
path = p.get("path", "")
|
|
2167
|
+
pid = p.get("pid")
|
|
2168
|
+
running = False
|
|
2169
|
+
if isinstance(pid, int) and pid > 0:
|
|
2170
|
+
try:
|
|
2171
|
+
os.kill(pid, 0)
|
|
2172
|
+
running = True # signal 0 delivered -> pid alive
|
|
2173
|
+
except PermissionError:
|
|
2174
|
+
running = True # pid exists but owned by another user
|
|
2175
|
+
except (ProcessLookupError, OSError):
|
|
2176
|
+
running = False # ESRCH -> dead
|
|
2177
|
+
# A project is also "live" if its .loki/session.json says running.
|
|
2178
|
+
if not running and path:
|
|
2179
|
+
try:
|
|
2180
|
+
sess = _Path(path) / ".loki" / "session.json"
|
|
2181
|
+
if sess.is_file():
|
|
2182
|
+
import json as _json
|
|
2183
|
+
s = _json.loads(sess.read_text())
|
|
2184
|
+
running = s.get("status") == "running"
|
|
2185
|
+
except Exception:
|
|
2186
|
+
pass
|
|
2187
|
+
# Compare via realpath: /api/focus resolves symlinks (e.g. macOS
|
|
2188
|
+
# /tmp -> /private/tmp) while the registry stores abspath, so a plain
|
|
2189
|
+
# abspath compare would never match a focused symlinked project.
|
|
2190
|
+
is_active = False
|
|
2191
|
+
if active and path:
|
|
2192
|
+
try:
|
|
2193
|
+
is_active = os.path.realpath(active) == os.path.realpath(path)
|
|
2194
|
+
except OSError:
|
|
2195
|
+
is_active = os.path.abspath(active) == os.path.abspath(path)
|
|
2196
|
+
out.append({
|
|
2197
|
+
"id": p.get("id"),
|
|
2198
|
+
"name": p.get("name") or (os.path.basename(path) if path else "project"),
|
|
2199
|
+
"path": path,
|
|
2200
|
+
"port": p.get("port"),
|
|
2201
|
+
"status": p.get("status"),
|
|
2202
|
+
"running": running,
|
|
2203
|
+
"is_active": is_active,
|
|
2204
|
+
})
|
|
2205
|
+
return {"projects": out, "active_project_dir": active}
|
|
2206
|
+
|
|
2207
|
+
|
|
2143
2208
|
# =============================================================================
|
|
2144
2209
|
# Enterprise Features (Optional - enabled via environment variables)
|
|
2145
2210
|
# =============================================================================
|
|
@@ -285,6 +285,25 @@
|
|
|
285
285
|
box-shadow: 0 0 0 3px var(--loki-accent-glow);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/* v7.7.29 multi-project switcher */
|
|
289
|
+
.project-switcher {
|
|
290
|
+
margin-left: 14px;
|
|
291
|
+
padding: 5px 10px;
|
|
292
|
+
background: var(--loki-bg-primary);
|
|
293
|
+
border: 1px solid var(--loki-border);
|
|
294
|
+
border-radius: 6px;
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
297
|
+
color: var(--loki-text-primary);
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
max-width: 280px;
|
|
300
|
+
}
|
|
301
|
+
.project-switcher:focus {
|
|
302
|
+
outline: none;
|
|
303
|
+
border-color: var(--loki-accent);
|
|
304
|
+
box-shadow: 0 0 0 3px var(--loki-accent-glow);
|
|
305
|
+
}
|
|
306
|
+
|
|
288
307
|
/* Main Content */
|
|
289
308
|
.main-content {
|
|
290
309
|
padding: 28px 32px;
|
|
@@ -532,6 +551,11 @@
|
|
|
532
551
|
</button>
|
|
533
552
|
<span class="logo-brand">Loki Mode</span>
|
|
534
553
|
<span class="logo-subtitle">powered by Autonomi</span>
|
|
554
|
+
<!-- v7.7.29 multi-project switcher: lists projects running loki in
|
|
555
|
+
different folders and switches which one the dashboard shows. -->
|
|
556
|
+
<select class="project-switcher" id="project-switcher" title="Switch project" aria-label="Switch project">
|
|
557
|
+
<option value="">All projects (current dir)</option>
|
|
558
|
+
</select>
|
|
535
559
|
</div>
|
|
536
560
|
|
|
537
561
|
<nav class="nav-links">
|
|
@@ -13373,6 +13397,49 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
13373
13397
|
var initResult = LokiDashboard.init({ autoDetectContext: true });
|
|
13374
13398
|
console.log('Loki Dashboard initialized:', initResult);
|
|
13375
13399
|
|
|
13400
|
+
// v7.7.29 multi-project switcher: populate from /api/running-projects and
|
|
13401
|
+
// switch the focused project via /api/focus. Fully best-effort; if the
|
|
13402
|
+
// endpoint is unavailable the dropdown simply stays at "All projects".
|
|
13403
|
+
(function initProjectSwitcher() {
|
|
13404
|
+
var sel = document.getElementById('project-switcher');
|
|
13405
|
+
if (!sel) return;
|
|
13406
|
+
function refresh() {
|
|
13407
|
+
fetch('/api/running-projects')
|
|
13408
|
+
.then(function(r){ return r.ok ? r.json() : null; })
|
|
13409
|
+
.then(function(data){
|
|
13410
|
+
if (!data || !Array.isArray(data.projects)) return;
|
|
13411
|
+
var current = sel.value;
|
|
13412
|
+
// Rebuild options: keep the "All projects" default first.
|
|
13413
|
+
sel.innerHTML = '';
|
|
13414
|
+
var optAll = document.createElement('option');
|
|
13415
|
+
optAll.value = ''; optAll.textContent = 'All projects (current dir)';
|
|
13416
|
+
sel.appendChild(optAll);
|
|
13417
|
+
data.projects.forEach(function(p){
|
|
13418
|
+
var o = document.createElement('option');
|
|
13419
|
+
o.value = p.path || '';
|
|
13420
|
+
var dot = p.running ? '* ' : ''; // running marker (ASCII)
|
|
13421
|
+
o.textContent = dot + (p.name || p.path || 'project');
|
|
13422
|
+
if (p.is_active) o.selected = true;
|
|
13423
|
+
sel.appendChild(o);
|
|
13424
|
+
});
|
|
13425
|
+
if (!data.active_project_dir && current === '') sel.value = '';
|
|
13426
|
+
})
|
|
13427
|
+
.catch(function(){ /* offline / no endpoint: leave as-is */ });
|
|
13428
|
+
}
|
|
13429
|
+
sel.addEventListener('change', function(){
|
|
13430
|
+
var dir = sel.value;
|
|
13431
|
+
var req = dir
|
|
13432
|
+
? fetch('/api/focus', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_dir: dir }) })
|
|
13433
|
+
: fetch('/api/focus', { method: 'DELETE' });
|
|
13434
|
+
req.then(function(){
|
|
13435
|
+
// Reload so every panel re-fetches against the newly focused project.
|
|
13436
|
+
window.location.reload();
|
|
13437
|
+
}).catch(function(){ /* ignore */ });
|
|
13438
|
+
});
|
|
13439
|
+
refresh();
|
|
13440
|
+
setInterval(refresh, 15000);
|
|
13441
|
+
})();
|
|
13442
|
+
|
|
13376
13443
|
// Theme toggle functionality
|
|
13377
13444
|
var themeToggle = document.getElementById('theme-toggle');
|
|
13378
13445
|
var themeLabel = document.getElementById('theme-label');
|