loki-mode 6.31.0 → 6.32.1
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 +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +665 -138
- package/bin/postinstall.js +24 -0
- package/completions/_loki +10 -0
- package/completions/loki.bash +19 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.autonomi.dev/)
|
|
11
11
|
[](https://hub.docker.com/r/asklokesh/loki-mode)
|
|
12
12
|
|
|
13
|
-
**Current Version: v6.
|
|
13
|
+
**Current Version: v6.32.0**
|
|
14
14
|
|
|
15
15
|
### Traction
|
|
16
16
|
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.32.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
267
267
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
268
268
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
269
269
|
|
|
270
|
-
**v6.
|
|
270
|
+
**v6.32.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.32.1
|
package/autonomy/loki
CHANGED
|
@@ -394,7 +394,7 @@ show_help() {
|
|
|
394
394
|
echo " demo Run interactive demo (~60s simulated session)"
|
|
395
395
|
echo " init [name] Project scaffolding with 22 PRD templates"
|
|
396
396
|
echo " issue <url|num> [DEPRECATED] Use 'loki run' instead"
|
|
397
|
-
echo " watch [
|
|
397
|
+
echo " watch [prd] Auto-rerun on PRD file changes (v6.33.0)"
|
|
398
398
|
echo " export <format> Export session data: json|markdown|csv|timeline (v6.0.0)"
|
|
399
399
|
echo " stop Stop execution immediately"
|
|
400
400
|
echo " cleanup Kill orphaned processes from crashed sessions"
|
|
@@ -424,7 +424,7 @@ show_help() {
|
|
|
424
424
|
echo " review [opts] Standalone code review with quality gates (diff, staged, PR, files)"
|
|
425
425
|
echo " optimize Optimize prompts based on session history"
|
|
426
426
|
echo " enterprise Enterprise feature management (tokens, OIDC)"
|
|
427
|
-
echo " metrics
|
|
427
|
+
echo " metrics [opts] Session productivity report (--json, --last N, --save, --share)"
|
|
428
428
|
echo " dogfood Show self-development statistics"
|
|
429
429
|
echo " secrets [cmd] API key status and validation (status|validate)"
|
|
430
430
|
echo " reset [target] Reset session state (all|retries|failed)"
|
|
@@ -487,7 +487,7 @@ show_help() {
|
|
|
487
487
|
echo " loki start ./prd.md # Start with PRD file"
|
|
488
488
|
echo " loki start --bg # Start in background"
|
|
489
489
|
echo " loki start --parallel # Start in parallel mode"
|
|
490
|
-
echo " loki watch #
|
|
490
|
+
echo " loki watch # Watch PRD for changes, auto-rerun"
|
|
491
491
|
echo " loki export json # Export session data"
|
|
492
492
|
echo " loki config set maxTier sonnet # Cap model cost"
|
|
493
493
|
echo " loki status # Check current status"
|
|
@@ -4312,126 +4312,252 @@ EOF
|
|
|
4312
4312
|
|
|
4313
4313
|
# Show configuration
|
|
4314
4314
|
#===============================================================================
|
|
4315
|
-
# loki watch -
|
|
4315
|
+
# loki watch - Auto-rerun on PRD file changes (v6.33.0)
|
|
4316
4316
|
#===============================================================================
|
|
4317
4317
|
|
|
4318
4318
|
cmd_watch() {
|
|
4319
|
-
local
|
|
4319
|
+
local prd_path=""
|
|
4320
|
+
local run_once=false
|
|
4321
|
+
local poll_interval=2
|
|
4322
|
+
local no_auto_start=false
|
|
4323
|
+
local debounce=3
|
|
4324
|
+
local _watch_child_pid=""
|
|
4320
4325
|
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4326
|
+
while [[ $# -gt 0 ]]; do
|
|
4327
|
+
case "$1" in
|
|
4328
|
+
--help|-h)
|
|
4329
|
+
echo -e "${BOLD}loki watch${NC} - Auto-rerun on PRD file changes (v6.33.0)"
|
|
4330
|
+
echo ""
|
|
4331
|
+
echo "Usage: loki watch [prd-path] [options]"
|
|
4332
|
+
echo ""
|
|
4333
|
+
echo "Monitors a PRD file for changes and automatically re-runs Loki Mode"
|
|
4334
|
+
echo "when the file is saved. Enables a tight edit-PRD-see-results loop."
|
|
4335
|
+
echo ""
|
|
4336
|
+
echo "Arguments:"
|
|
4337
|
+
echo " prd-path Path to PRD file (default: auto-detect prd.md, PRD.md, or first *.md)"
|
|
4338
|
+
echo ""
|
|
4339
|
+
echo "Options:"
|
|
4340
|
+
echo " --once Run once immediately then exit"
|
|
4341
|
+
echo " --interval N Poll interval in seconds for fallback watcher (default: 2)"
|
|
4342
|
+
echo " --no-auto-start Watch but do not auto-start, just print change timestamps"
|
|
4343
|
+
echo " --debounce N Wait N seconds after change before triggering (default: 3)"
|
|
4344
|
+
echo " --help, -h Show this help"
|
|
4345
|
+
echo ""
|
|
4346
|
+
echo "File watcher priority:"
|
|
4347
|
+
echo " 1. fswatch (macOS) -- native filesystem events"
|
|
4348
|
+
echo " 2. inotifywait (Linux) -- inotify-based"
|
|
4349
|
+
echo " 3. stat polling -- universal fallback"
|
|
4350
|
+
echo ""
|
|
4351
|
+
echo "Examples:"
|
|
4352
|
+
echo " loki watch # Auto-detect PRD and watch"
|
|
4353
|
+
echo " loki watch ./my-prd.md # Watch specific file"
|
|
4354
|
+
echo " loki watch --once # Run once immediately then exit"
|
|
4355
|
+
echo " loki watch --no-auto-start # Just report changes, don't run"
|
|
4356
|
+
echo " loki watch --debounce 5 # Wait 5s after change before triggering"
|
|
4357
|
+
echo " loki watch --interval 1 # Poll every 1s (fallback mode)"
|
|
4358
|
+
return 0
|
|
4359
|
+
;;
|
|
4360
|
+
--once) run_once=true; shift ;;
|
|
4361
|
+
--interval) poll_interval="${2:-2}"; shift 2 ;;
|
|
4362
|
+
--interval=*) poll_interval="${1#*=}"; shift ;;
|
|
4363
|
+
--no-auto-start) no_auto_start=true; shift ;;
|
|
4364
|
+
--debounce) debounce="${2:-3}"; shift 2 ;;
|
|
4365
|
+
--debounce=*) debounce="${1#*=}"; shift ;;
|
|
4366
|
+
-*)
|
|
4367
|
+
echo -e "${RED}Unknown option: $1${NC}"
|
|
4368
|
+
echo "Run 'loki watch --help' for usage."
|
|
4369
|
+
return 1
|
|
4370
|
+
;;
|
|
4371
|
+
*)
|
|
4372
|
+
if [ -z "$prd_path" ]; then
|
|
4373
|
+
prd_path="$1"
|
|
4374
|
+
else
|
|
4375
|
+
echo -e "${RED}Unexpected argument: $1${NC}"
|
|
4376
|
+
return 1
|
|
4377
|
+
fi
|
|
4378
|
+
shift
|
|
4379
|
+
;;
|
|
4380
|
+
esac
|
|
4381
|
+
done
|
|
4382
|
+
|
|
4383
|
+
# Validate numeric arguments
|
|
4384
|
+
if ! [[ "$poll_interval" =~ ^[0-9]+$ ]] || [ "$poll_interval" -lt 1 ]; then
|
|
4385
|
+
echo -e "${RED}Invalid --interval: $poll_interval (expected positive integer)${NC}"
|
|
4330
4386
|
return 1
|
|
4331
4387
|
fi
|
|
4332
|
-
if [[ "$
|
|
4333
|
-
echo -e "${
|
|
4334
|
-
|
|
4335
|
-
echo "Usage: loki watch [interval]"
|
|
4336
|
-
echo ""
|
|
4337
|
-
echo "Options:"
|
|
4338
|
-
echo " interval Refresh interval in seconds (default: 2)"
|
|
4339
|
-
echo ""
|
|
4340
|
-
echo "Displays:"
|
|
4341
|
-
echo " - Current iteration and RARV phase"
|
|
4342
|
-
echo " - Provider and model tier"
|
|
4343
|
-
echo " - Task queue status"
|
|
4344
|
-
echo " - Recent log lines"
|
|
4345
|
-
echo " - Cost tracking"
|
|
4346
|
-
echo ""
|
|
4347
|
-
echo "Press Ctrl+C to exit."
|
|
4348
|
-
exit 0
|
|
4388
|
+
if ! [[ "$debounce" =~ ^[0-9]+$ ]] || [ "$debounce" -lt 0 ]; then
|
|
4389
|
+
echo -e "${RED}Invalid --debounce: $debounce (expected non-negative integer)${NC}"
|
|
4390
|
+
return 1
|
|
4349
4391
|
fi
|
|
4350
4392
|
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4393
|
+
# Auto-detect PRD file if not specified
|
|
4394
|
+
if [ -z "$prd_path" ]; then
|
|
4395
|
+
if [ -f "prd.md" ]; then
|
|
4396
|
+
prd_path="prd.md"
|
|
4397
|
+
elif [ -f "PRD.md" ]; then
|
|
4398
|
+
prd_path="PRD.md"
|
|
4399
|
+
else
|
|
4400
|
+
# Find first .md file in current directory
|
|
4401
|
+
local first_md
|
|
4402
|
+
first_md=$(ls -1 *.md 2>/dev/null | head -1 || true)
|
|
4403
|
+
if [ -n "$first_md" ]; then
|
|
4404
|
+
prd_path="$first_md"
|
|
4405
|
+
else
|
|
4406
|
+
echo -e "${RED}No PRD file found.${NC}"
|
|
4407
|
+
echo "Specify a file: loki watch ./my-prd.md"
|
|
4408
|
+
echo "Or create one: prd.md, PRD.md, or any .md file in current directory."
|
|
4409
|
+
return 1
|
|
4410
|
+
fi
|
|
4411
|
+
fi
|
|
4355
4412
|
fi
|
|
4356
4413
|
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4414
|
+
if [ ! -f "$prd_path" ]; then
|
|
4415
|
+
echo -e "${RED}PRD file not found: $prd_path${NC}"
|
|
4416
|
+
return 1
|
|
4417
|
+
fi
|
|
4361
4418
|
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4419
|
+
# Resolve to absolute path
|
|
4420
|
+
prd_path="$(cd "$(dirname "$prd_path")" && pwd)/$(basename "$prd_path")"
|
|
4421
|
+
local prd_basename
|
|
4422
|
+
prd_basename="$(basename "$prd_path")"
|
|
4423
|
+
|
|
4424
|
+
# --once mode: run immediately and exit
|
|
4425
|
+
if [ "$run_once" = true ]; then
|
|
4426
|
+
echo -e "${BOLD}loki watch${NC} --once: running loki start ${prd_basename}"
|
|
4427
|
+
"$0" start "$prd_path"
|
|
4428
|
+
return $?
|
|
4429
|
+
fi
|
|
4366
4430
|
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
echo -e "Status: ${DIM}IDLE${NC}"
|
|
4431
|
+
# Graceful shutdown handler
|
|
4432
|
+
_watch_cleanup() {
|
|
4433
|
+
echo ""
|
|
4434
|
+
echo "Watch stopped."
|
|
4435
|
+
if [ -n "$_watch_child_pid" ] && kill -0 "$_watch_child_pid" 2>/dev/null; then
|
|
4436
|
+
echo "Stopping running loki session (PID $_watch_child_pid)..."
|
|
4437
|
+
kill "$_watch_child_pid" 2>/dev/null
|
|
4438
|
+
wait "$_watch_child_pid" 2>/dev/null || true
|
|
4376
4439
|
fi
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
iter=$(cat "$LOKI_DIR/state/iteration" 2>/dev/null || echo "?")
|
|
4382
|
-
echo -e "Iteration: $iter"
|
|
4440
|
+
# Kill any background watcher process
|
|
4441
|
+
if [ -n "${_watch_bg_pid:-}" ] && kill -0 "$_watch_bg_pid" 2>/dev/null; then
|
|
4442
|
+
kill "$_watch_bg_pid" 2>/dev/null
|
|
4443
|
+
wait "$_watch_bg_pid" 2>/dev/null || true
|
|
4383
4444
|
fi
|
|
4445
|
+
exit 0
|
|
4446
|
+
}
|
|
4447
|
+
trap '_watch_cleanup' INT TERM
|
|
4384
4448
|
|
|
4385
|
-
|
|
4386
|
-
local prov="${LOKI_PROVIDER:-claude}"
|
|
4387
|
-
if [ -f "$LOKI_DIR/state/provider" ]; then
|
|
4388
|
-
prov=$(cat "$LOKI_DIR/state/provider" 2>/dev/null || echo "$prov")
|
|
4389
|
-
fi
|
|
4390
|
-
echo -e "Provider: $prov"
|
|
4449
|
+
local last_run="never"
|
|
4391
4450
|
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
count=$(_QUEUE_FILE="$queue_file" python3 -c "import json, os; print(len(json.load(open(os.environ['_QUEUE_FILE']))))" 2>/dev/null || echo "0")
|
|
4400
|
-
echo " $queue: $count"
|
|
4401
|
-
fi
|
|
4402
|
-
done
|
|
4451
|
+
echo -e "${BOLD}loki watch${NC} -- monitoring ${prd_basename} for changes"
|
|
4452
|
+
echo "Watching: $prd_path"
|
|
4453
|
+
echo "Debounce: ${debounce}s | Ctrl+C to exit"
|
|
4454
|
+
if [ "$no_auto_start" = true ]; then
|
|
4455
|
+
echo "Mode: observe only (--no-auto-start)"
|
|
4456
|
+
fi
|
|
4457
|
+
echo ""
|
|
4403
4458
|
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
if budget > 0:
|
|
4416
|
-
print(f' Budget: \${budget:.2f} ({total/budget*100:.1f}% used)')
|
|
4417
|
-
" 2>/dev/null || true
|
|
4459
|
+
_watch_status_line() {
|
|
4460
|
+
echo -e "\rWatching ${prd_basename} -- last run: ${last_run} -- next: on change"
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
_watch_trigger() {
|
|
4464
|
+
local change_time
|
|
4465
|
+
change_time="$(date '+%H:%M:%S')"
|
|
4466
|
+
|
|
4467
|
+
if [ "$no_auto_start" = true ]; then
|
|
4468
|
+
echo "PRD changed: ${change_time}"
|
|
4469
|
+
return 0
|
|
4418
4470
|
fi
|
|
4419
4471
|
|
|
4420
|
-
#
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
else
|
|
4427
|
-
echo " (no log file)"
|
|
4472
|
+
# Kill any currently running loki session
|
|
4473
|
+
if [ -n "$_watch_child_pid" ] && kill -0 "$_watch_child_pid" 2>/dev/null; then
|
|
4474
|
+
echo "Stopping previous loki session (PID $_watch_child_pid)..."
|
|
4475
|
+
kill "$_watch_child_pid" 2>/dev/null
|
|
4476
|
+
wait "$_watch_child_pid" 2>/dev/null || true
|
|
4477
|
+
_watch_child_pid=""
|
|
4428
4478
|
fi
|
|
4429
4479
|
|
|
4430
4480
|
echo ""
|
|
4431
4481
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
4482
|
+
echo "PRD changed -- starting loki... (${change_time})"
|
|
4483
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
4484
|
+
echo ""
|
|
4432
4485
|
|
|
4433
|
-
|
|
4434
|
-
|
|
4486
|
+
"$0" start "$prd_path" &
|
|
4487
|
+
_watch_child_pid=$!
|
|
4488
|
+
last_run="$change_time"
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
# Determine watcher backend
|
|
4492
|
+
local watcher="poll"
|
|
4493
|
+
if command -v fswatch &>/dev/null; then
|
|
4494
|
+
watcher="fswatch"
|
|
4495
|
+
elif command -v inotifywait &>/dev/null; then
|
|
4496
|
+
watcher="inotifywait"
|
|
4497
|
+
fi
|
|
4498
|
+
|
|
4499
|
+
echo "Watcher: $watcher"
|
|
4500
|
+
_watch_status_line
|
|
4501
|
+
|
|
4502
|
+
case "$watcher" in
|
|
4503
|
+
fswatch)
|
|
4504
|
+
# Use fswatch for native macOS filesystem events
|
|
4505
|
+
fswatch -1 --latency "$debounce" "$prd_path" | while read -r _event; do
|
|
4506
|
+
_watch_trigger
|
|
4507
|
+
_watch_status_line
|
|
4508
|
+
# Re-attach fswatch for next change
|
|
4509
|
+
fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
|
|
4510
|
+
done
|
|
4511
|
+
# Fallback: loop-based fswatch for continuous watching
|
|
4512
|
+
while true; do
|
|
4513
|
+
fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1
|
|
4514
|
+
_watch_trigger
|
|
4515
|
+
_watch_status_line
|
|
4516
|
+
done
|
|
4517
|
+
;;
|
|
4518
|
+
inotifywait)
|
|
4519
|
+
while true; do
|
|
4520
|
+
inotifywait -qq -e modify "$prd_path" 2>/dev/null
|
|
4521
|
+
if [ "$debounce" -gt 0 ]; then
|
|
4522
|
+
sleep "$debounce"
|
|
4523
|
+
fi
|
|
4524
|
+
_watch_trigger
|
|
4525
|
+
_watch_status_line
|
|
4526
|
+
done
|
|
4527
|
+
;;
|
|
4528
|
+
poll)
|
|
4529
|
+
# Universal fallback: stat-based polling
|
|
4530
|
+
local last_mtime
|
|
4531
|
+
last_mtime=$(stat -f '%m' "$prd_path" 2>/dev/null || stat -c '%Y' "$prd_path" 2>/dev/null || echo "0")
|
|
4532
|
+
local debounce_pending=false
|
|
4533
|
+
local debounce_deadline=0
|
|
4534
|
+
|
|
4535
|
+
while true; do
|
|
4536
|
+
sleep "$poll_interval"
|
|
4537
|
+
local current_mtime
|
|
4538
|
+
current_mtime=$(stat -f '%m' "$prd_path" 2>/dev/null || stat -c '%Y' "$prd_path" 2>/dev/null || echo "0")
|
|
4539
|
+
|
|
4540
|
+
if [ "$current_mtime" != "$last_mtime" ]; then
|
|
4541
|
+
last_mtime="$current_mtime"
|
|
4542
|
+
if [ "$debounce" -gt 0 ]; then
|
|
4543
|
+
# Start debounce window
|
|
4544
|
+
debounce_pending=true
|
|
4545
|
+
debounce_deadline=$(( $(date +%s) + debounce ))
|
|
4546
|
+
else
|
|
4547
|
+
_watch_trigger
|
|
4548
|
+
_watch_status_line
|
|
4549
|
+
fi
|
|
4550
|
+
fi
|
|
4551
|
+
|
|
4552
|
+
# Check debounce deadline
|
|
4553
|
+
if [ "$debounce_pending" = true ] && [ "$(date +%s)" -ge "$debounce_deadline" ]; then
|
|
4554
|
+
debounce_pending=false
|
|
4555
|
+
_watch_trigger
|
|
4556
|
+
_watch_status_line
|
|
4557
|
+
fi
|
|
4558
|
+
done
|
|
4559
|
+
;;
|
|
4560
|
+
esac
|
|
4435
4561
|
}
|
|
4436
4562
|
|
|
4437
4563
|
#===============================================================================
|
|
@@ -13833,47 +13959,448 @@ cmd_syslog() {
|
|
|
13833
13959
|
|
|
13834
13960
|
# Fetch and display Prometheus metrics from dashboard
|
|
13835
13961
|
cmd_metrics() {
|
|
13836
|
-
local
|
|
13837
|
-
local
|
|
13838
|
-
local
|
|
13962
|
+
local show_json=false
|
|
13963
|
+
local last_n=0
|
|
13964
|
+
local save_file=false
|
|
13965
|
+
local share_flag=false
|
|
13839
13966
|
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
echo
|
|
13866
|
-
echo "
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
;;
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13967
|
+
while [[ $# -gt 0 ]]; do
|
|
13968
|
+
case "$1" in
|
|
13969
|
+
--help|-h)
|
|
13970
|
+
echo -e "${BOLD}loki metrics${NC} - Session productivity reporter"
|
|
13971
|
+
echo ""
|
|
13972
|
+
echo "Usage: loki metrics [options]"
|
|
13973
|
+
echo ""
|
|
13974
|
+
echo "Analyzes past Loki Mode session data and generates a productivity"
|
|
13975
|
+
echo "stats report showing agents deployed, iterations completed, files"
|
|
13976
|
+
echo "changed, and estimated time saved."
|
|
13977
|
+
echo ""
|
|
13978
|
+
echo "Options:"
|
|
13979
|
+
echo " --json Machine-readable JSON output"
|
|
13980
|
+
echo " --last N Analyze only the last N sessions (default: all)"
|
|
13981
|
+
echo " --save Write report to METRICS.md in project root"
|
|
13982
|
+
echo " --share Upload report as GitHub Gist (via loki share)"
|
|
13983
|
+
echo " --help, -h Show this help"
|
|
13984
|
+
echo ""
|
|
13985
|
+
echo "Subcommands:"
|
|
13986
|
+
echo " prometheus Fetch Prometheus/OpenMetrics from dashboard"
|
|
13987
|
+
echo ""
|
|
13988
|
+
echo "Examples:"
|
|
13989
|
+
echo " loki metrics # Full productivity report"
|
|
13990
|
+
echo " loki metrics --json # Machine-readable output"
|
|
13991
|
+
echo " loki metrics --last 5 # Last 5 sessions only"
|
|
13992
|
+
echo " loki metrics --save # Write METRICS.md"
|
|
13993
|
+
echo " loki metrics --share # Share as GitHub Gist"
|
|
13994
|
+
echo " loki metrics prometheus # Prometheus metrics from dashboard"
|
|
13995
|
+
exit 0
|
|
13996
|
+
;;
|
|
13997
|
+
--json) show_json=true; shift ;;
|
|
13998
|
+
--last) last_n="${2:-0}"; shift 2 ;;
|
|
13999
|
+
--last=*) last_n="${1#*=}"; shift ;;
|
|
14000
|
+
--save) save_file=true; shift ;;
|
|
14001
|
+
--share) share_flag=true; shift ;;
|
|
14002
|
+
prometheus)
|
|
14003
|
+
# Legacy Prometheus metrics subcommand
|
|
14004
|
+
shift
|
|
14005
|
+
local port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
14006
|
+
local host="127.0.0.1"
|
|
14007
|
+
local url="http://${host}:${port}/metrics"
|
|
14008
|
+
local response
|
|
14009
|
+
response=$(curl -sf "$url" 2>/dev/null) || true
|
|
14010
|
+
if [ -z "$response" ]; then
|
|
14011
|
+
echo -e "${RED}Error: Could not connect to dashboard at ${url}${NC}"
|
|
14012
|
+
echo "Make sure the dashboard is running: loki serve"
|
|
14013
|
+
exit 1
|
|
14014
|
+
fi
|
|
14015
|
+
echo "$response"
|
|
14016
|
+
exit 0
|
|
14017
|
+
;;
|
|
14018
|
+
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki metrics --help' for usage."; exit 1 ;;
|
|
14019
|
+
esac
|
|
14020
|
+
done
|
|
14021
|
+
|
|
14022
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
14023
|
+
|
|
14024
|
+
if ! command -v python3 &>/dev/null; then
|
|
14025
|
+
echo -e "${RED}python3 is required for metrics generation${NC}"
|
|
14026
|
+
exit 1
|
|
14027
|
+
fi
|
|
14028
|
+
|
|
14029
|
+
local metrics_output
|
|
14030
|
+
metrics_output=$(LOKI_DIR="$loki_dir" \
|
|
14031
|
+
METRICS_JSON="$show_json" \
|
|
14032
|
+
METRICS_LAST_N="$last_n" \
|
|
14033
|
+
METRICS_SAVE="$save_file" \
|
|
14034
|
+
python3 << 'METRICS_SCRIPT'
|
|
14035
|
+
import json
|
|
14036
|
+
import os
|
|
14037
|
+
import sys
|
|
14038
|
+
import glob
|
|
14039
|
+
import subprocess
|
|
14040
|
+
from datetime import datetime
|
|
14041
|
+
|
|
14042
|
+
loki_dir = os.environ.get("LOKI_DIR", ".loki")
|
|
14043
|
+
show_json = os.environ.get("METRICS_JSON", "false") == "true"
|
|
14044
|
+
last_n = int(os.environ.get("METRICS_LAST_N", "0"))
|
|
14045
|
+
save_flag = os.environ.get("METRICS_SAVE", "false") == "true"
|
|
14046
|
+
|
|
14047
|
+
def load_json(path):
|
|
14048
|
+
try:
|
|
14049
|
+
with open(path) as f:
|
|
14050
|
+
return json.load(f)
|
|
14051
|
+
except Exception:
|
|
14052
|
+
return None
|
|
14053
|
+
|
|
14054
|
+
def fmt_number(n):
|
|
14055
|
+
return f"{n:,}"
|
|
14056
|
+
|
|
14057
|
+
def load_queue(path):
|
|
14058
|
+
data = load_json(path)
|
|
14059
|
+
if isinstance(data, list):
|
|
14060
|
+
return data
|
|
14061
|
+
if isinstance(data, dict) and "tasks" in data:
|
|
14062
|
+
return data["tasks"]
|
|
14063
|
+
return []
|
|
14064
|
+
|
|
14065
|
+
# --- Gather project name ---
|
|
14066
|
+
project_name = os.path.basename(os.getcwd())
|
|
14067
|
+
|
|
14068
|
+
# --- Session data ---
|
|
14069
|
+
sessions_analyzed = 0
|
|
14070
|
+
total_iterations = 0
|
|
14071
|
+
total_tasks_completed = 0
|
|
14072
|
+
total_tasks_failed = 0
|
|
14073
|
+
agent_types_seen = set()
|
|
14074
|
+
total_duration_seconds = 0
|
|
14075
|
+
|
|
14076
|
+
# Count sessions from checkpoints index or autonomy-state
|
|
14077
|
+
checkpoint_index = os.path.join(loki_dir, "state", "checkpoints", "index.jsonl")
|
|
14078
|
+
session_entries = []
|
|
14079
|
+
if os.path.isfile(checkpoint_index):
|
|
14080
|
+
try:
|
|
14081
|
+
with open(checkpoint_index) as f:
|
|
14082
|
+
for line in f:
|
|
14083
|
+
line = line.strip()
|
|
14084
|
+
if line:
|
|
14085
|
+
try:
|
|
14086
|
+
session_entries.append(json.loads(line))
|
|
14087
|
+
except Exception:
|
|
14088
|
+
pass
|
|
14089
|
+
except Exception:
|
|
14090
|
+
pass
|
|
14091
|
+
|
|
14092
|
+
# Orchestrator state
|
|
14093
|
+
orch = load_json(os.path.join(loki_dir, "state", "orchestrator.json")) or {}
|
|
14094
|
+
current_phase = orch.get("currentPhase", "N/A")
|
|
14095
|
+
orch_metrics = orch.get("metrics", {})
|
|
14096
|
+
tasks_completed_orch = orch_metrics.get("tasksCompleted", 0)
|
|
14097
|
+
tasks_failed_orch = orch_metrics.get("tasksFailed", 0)
|
|
14098
|
+
|
|
14099
|
+
# Per-iteration efficiency files
|
|
14100
|
+
eff_dir = os.path.join(loki_dir, "metrics", "efficiency")
|
|
14101
|
+
iterations = []
|
|
14102
|
+
if os.path.isdir(eff_dir):
|
|
14103
|
+
for path in sorted(glob.glob(os.path.join(eff_dir, "iteration-*.json"))):
|
|
14104
|
+
data = load_json(path)
|
|
14105
|
+
if data:
|
|
14106
|
+
iterations.append(data)
|
|
14107
|
+
|
|
14108
|
+
# Apply --last N filter
|
|
14109
|
+
if last_n > 0 and len(iterations) > last_n:
|
|
14110
|
+
iterations = iterations[-last_n:]
|
|
14111
|
+
|
|
14112
|
+
total_iterations = len(iterations) if iterations else max(orch.get("currentIteration", 0), 0)
|
|
14113
|
+
|
|
14114
|
+
# Gather agent types from iterations and agents.json
|
|
14115
|
+
for it in iterations:
|
|
14116
|
+
model = it.get("model", "")
|
|
14117
|
+
if model:
|
|
14118
|
+
agent_types_seen.add(model)
|
|
14119
|
+
total_duration_seconds += it.get("duration_seconds", 0)
|
|
14120
|
+
|
|
14121
|
+
agents_file = os.path.join(loki_dir, "state", "agents.json")
|
|
14122
|
+
agents_data = load_json(agents_file)
|
|
14123
|
+
if isinstance(agents_data, list):
|
|
14124
|
+
for agent in agents_data:
|
|
14125
|
+
atype = agent.get("agent_type", "")
|
|
14126
|
+
if atype:
|
|
14127
|
+
agent_types_seen.add(atype)
|
|
14128
|
+
|
|
14129
|
+
# Queue data for task counts
|
|
14130
|
+
completed_tasks = load_queue(os.path.join(loki_dir, "queue", "completed.json"))
|
|
14131
|
+
failed_tasks = load_queue(os.path.join(loki_dir, "queue", "failed.json"))
|
|
14132
|
+
pending_tasks = load_queue(os.path.join(loki_dir, "queue", "pending.json"))
|
|
14133
|
+
in_progress_tasks = load_queue(os.path.join(loki_dir, "queue", "in-progress.json"))
|
|
14134
|
+
|
|
14135
|
+
total_tasks_completed = len(completed_tasks) if completed_tasks else tasks_completed_orch
|
|
14136
|
+
total_tasks_failed = len(failed_tasks) if failed_tasks else tasks_failed_orch
|
|
14137
|
+
total_tasks = total_tasks_completed + total_tasks_failed + len(pending_tasks) + len(in_progress_tasks)
|
|
14138
|
+
success_rate = (total_tasks_completed / total_tasks * 100) if total_tasks > 0 else 0.0
|
|
14139
|
+
|
|
14140
|
+
# Session count: count unique session directories or fall back to 1 if .loki exists
|
|
14141
|
+
sessions_dir = os.path.join(loki_dir, "state", "sessions")
|
|
14142
|
+
if os.path.isdir(sessions_dir):
|
|
14143
|
+
sessions_analyzed = len([d for d in os.listdir(sessions_dir) if os.path.isdir(os.path.join(sessions_dir, d))])
|
|
14144
|
+
if sessions_analyzed == 0:
|
|
14145
|
+
# Check autonomy-state.json retryCount as proxy for session attempts
|
|
14146
|
+
auto_state = load_json(os.path.join(loki_dir, "autonomy-state.json"))
|
|
14147
|
+
if auto_state:
|
|
14148
|
+
sessions_analyzed = max(1, auto_state.get("retryCount", 1))
|
|
14149
|
+
elif os.path.isdir(loki_dir):
|
|
14150
|
+
sessions_analyzed = 1
|
|
14151
|
+
|
|
14152
|
+
# Apply --last N to sessions
|
|
14153
|
+
if last_n > 0:
|
|
14154
|
+
sessions_analyzed = min(sessions_analyzed, last_n)
|
|
14155
|
+
|
|
14156
|
+
# --- Git stats ---
|
|
14157
|
+
lines_added = 0
|
|
14158
|
+
lines_removed = 0
|
|
14159
|
+
commits_made = 0
|
|
14160
|
+
files_changed_git = 0
|
|
14161
|
+
tests_written = 0
|
|
14162
|
+
|
|
14163
|
+
try:
|
|
14164
|
+
result = subprocess.run(
|
|
14165
|
+
["git", "log", "--shortstat", "--format="],
|
|
14166
|
+
capture_output=True, text=True, timeout=10
|
|
14167
|
+
)
|
|
14168
|
+
if result.returncode == 0:
|
|
14169
|
+
for line in result.stdout.strip().split("\n"):
|
|
14170
|
+
line = line.strip()
|
|
14171
|
+
if not line:
|
|
14172
|
+
continue
|
|
14173
|
+
parts = line.split(",")
|
|
14174
|
+
for part in parts:
|
|
14175
|
+
part = part.strip()
|
|
14176
|
+
if "file" in part and "changed" in part:
|
|
14177
|
+
try:
|
|
14178
|
+
files_changed_git += int(part.split()[0])
|
|
14179
|
+
except (ValueError, IndexError):
|
|
14180
|
+
pass
|
|
14181
|
+
elif "insertion" in part:
|
|
14182
|
+
try:
|
|
14183
|
+
lines_added += int(part.split()[0])
|
|
14184
|
+
except (ValueError, IndexError):
|
|
14185
|
+
pass
|
|
14186
|
+
elif "deletion" in part:
|
|
14187
|
+
try:
|
|
14188
|
+
lines_removed += int(part.split()[0])
|
|
14189
|
+
except (ValueError, IndexError):
|
|
14190
|
+
pass
|
|
14191
|
+
|
|
14192
|
+
# Count commits
|
|
14193
|
+
result2 = subprocess.run(
|
|
14194
|
+
["git", "rev-list", "--count", "HEAD"],
|
|
14195
|
+
capture_output=True, text=True, timeout=10
|
|
14196
|
+
)
|
|
14197
|
+
if result2.returncode == 0:
|
|
14198
|
+
commits_made = int(result2.stdout.strip())
|
|
14199
|
+
except Exception:
|
|
14200
|
+
pass
|
|
14201
|
+
|
|
14202
|
+
# Estimate tests written from git log (files matching test/spec patterns)
|
|
14203
|
+
try:
|
|
14204
|
+
result3 = subprocess.run(
|
|
14205
|
+
["git", "log", "--diff-filter=A", "--name-only", "--format="],
|
|
14206
|
+
capture_output=True, text=True, timeout=10
|
|
14207
|
+
)
|
|
14208
|
+
if result3.returncode == 0:
|
|
14209
|
+
for fname in result3.stdout.strip().split("\n"):
|
|
14210
|
+
fname = fname.strip().lower()
|
|
14211
|
+
if any(kw in fname for kw in ["test", "spec", ".test.", ".spec.", "_test.", "_spec."]):
|
|
14212
|
+
tests_written += 1
|
|
14213
|
+
except Exception:
|
|
14214
|
+
pass
|
|
14215
|
+
|
|
14216
|
+
# --- Token/cost data ---
|
|
14217
|
+
total_cost = sum(it.get("cost_usd", 0) for it in iterations)
|
|
14218
|
+
total_input_tokens = sum(it.get("input_tokens", 0) for it in iterations)
|
|
14219
|
+
total_output_tokens = sum(it.get("output_tokens", 0) for it in iterations)
|
|
14220
|
+
|
|
14221
|
+
# Also check context tracking for cost data
|
|
14222
|
+
ctx_file = os.path.join(loki_dir, "context", "tracking.json")
|
|
14223
|
+
ctx = load_json(ctx_file)
|
|
14224
|
+
if ctx and total_cost == 0:
|
|
14225
|
+
totals = ctx.get("totals", {})
|
|
14226
|
+
total_cost = totals.get("total_cost_usd", 0)
|
|
14227
|
+
total_input_tokens = totals.get("total_input", 0)
|
|
14228
|
+
total_output_tokens = totals.get("total_output", 0)
|
|
14229
|
+
|
|
14230
|
+
# --- Time saved estimate ---
|
|
14231
|
+
# Each Loki iteration replaces ~15min of manual work
|
|
14232
|
+
time_saved_hours = round(total_iterations * 15 / 60, 2)
|
|
14233
|
+
|
|
14234
|
+
# --- Memory stats ---
|
|
14235
|
+
episodic_count = 0
|
|
14236
|
+
semantic_count = 0
|
|
14237
|
+
episodic_dir = os.path.join(loki_dir, "memory", "episodic")
|
|
14238
|
+
semantic_dir = os.path.join(loki_dir, "memory", "semantic")
|
|
14239
|
+
if os.path.isdir(episodic_dir):
|
|
14240
|
+
for root, dirs, files in os.walk(episodic_dir):
|
|
14241
|
+
episodic_count += len([f for f in files if f.endswith(".json")])
|
|
14242
|
+
if os.path.isdir(semantic_dir):
|
|
14243
|
+
for root, dirs, files in os.walk(semantic_dir):
|
|
14244
|
+
semantic_count += len([f for f in files if f.endswith(".json")])
|
|
14245
|
+
|
|
14246
|
+
# --- Build output ---
|
|
14247
|
+
version = "6.32.0"
|
|
14248
|
+
try:
|
|
14249
|
+
version_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "VERSION")
|
|
14250
|
+
# In heredoc context, __file__ is not reliable; try relative
|
|
14251
|
+
for vf in ["VERSION", "../VERSION", os.path.join(loki_dir, "..", "VERSION")]:
|
|
14252
|
+
if os.path.isfile(vf):
|
|
14253
|
+
with open(vf) as f:
|
|
14254
|
+
version = f.read().strip()
|
|
14255
|
+
break
|
|
14256
|
+
except Exception:
|
|
14257
|
+
pass
|
|
14258
|
+
|
|
14259
|
+
data = {
|
|
14260
|
+
"project": project_name,
|
|
14261
|
+
"sessions_analyzed": sessions_analyzed,
|
|
14262
|
+
"agent_activity": {
|
|
14263
|
+
"total_iterations": total_iterations,
|
|
14264
|
+
"agent_types": len(agent_types_seen),
|
|
14265
|
+
"tasks_completed": total_tasks_completed,
|
|
14266
|
+
"tasks_failed": total_tasks_failed,
|
|
14267
|
+
"success_rate": round(success_rate, 1)
|
|
14268
|
+
},
|
|
14269
|
+
"code_output": {
|
|
14270
|
+
"files_changed": files_changed_git,
|
|
14271
|
+
"lines_added": lines_added,
|
|
14272
|
+
"lines_removed": lines_removed,
|
|
14273
|
+
"commits": commits_made,
|
|
14274
|
+
"tests_written": tests_written
|
|
14275
|
+
},
|
|
14276
|
+
"tokens": {
|
|
14277
|
+
"input": total_input_tokens,
|
|
14278
|
+
"output": total_output_tokens,
|
|
14279
|
+
"total": total_input_tokens + total_output_tokens,
|
|
14280
|
+
"cost_usd": round(total_cost, 2)
|
|
14281
|
+
},
|
|
14282
|
+
"time_saved": {
|
|
14283
|
+
"hours": time_saved_hours,
|
|
14284
|
+
"iterations": total_iterations,
|
|
14285
|
+
"minutes_per_iteration": 15
|
|
14286
|
+
},
|
|
14287
|
+
"memory": {
|
|
14288
|
+
"episodic_entries": episodic_count,
|
|
14289
|
+
"semantic_patterns": semantic_count
|
|
14290
|
+
},
|
|
14291
|
+
"version": version
|
|
14292
|
+
}
|
|
14293
|
+
|
|
14294
|
+
if show_json:
|
|
14295
|
+
print(json.dumps(data, indent=2))
|
|
14296
|
+
else:
|
|
14297
|
+
# ASCII stats card
|
|
14298
|
+
lines = []
|
|
14299
|
+
lines.append("")
|
|
14300
|
+
lines.append("LOKI MODE PRODUCTIVITY REPORT")
|
|
14301
|
+
lines.append("=" * 40)
|
|
14302
|
+
lines.append(f"Project: {project_name}")
|
|
14303
|
+
lines.append(f"Sessions analyzed: {sessions_analyzed}")
|
|
14304
|
+
lines.append("")
|
|
14305
|
+
lines.append("AGENT ACTIVITY")
|
|
14306
|
+
lines.append(f" Total iterations: {fmt_number(total_iterations)}")
|
|
14307
|
+
lines.append(f" Agent types used: {len(agent_types_seen)}")
|
|
14308
|
+
lines.append(f" Tasks completed: {fmt_number(total_tasks_completed)}")
|
|
14309
|
+
if total_tasks_failed > 0:
|
|
14310
|
+
lines.append(f" Tasks failed: {fmt_number(total_tasks_failed)}")
|
|
14311
|
+
if total_tasks > 0:
|
|
14312
|
+
lines.append(f" Success rate: {success_rate:.1f}%")
|
|
14313
|
+
lines.append("")
|
|
14314
|
+
lines.append("CODE OUTPUT")
|
|
14315
|
+
lines.append(f" Files changed: {fmt_number(files_changed_git)}")
|
|
14316
|
+
lines.append(f" Lines added: {fmt_number(lines_added)}")
|
|
14317
|
+
lines.append(f" Lines removed: {fmt_number(lines_removed)}")
|
|
14318
|
+
lines.append(f" Commits: {fmt_number(commits_made)}")
|
|
14319
|
+
lines.append(f" Tests written: {fmt_number(tests_written)}")
|
|
14320
|
+
lines.append("")
|
|
14321
|
+
if total_cost > 0 or total_input_tokens > 0:
|
|
14322
|
+
lines.append("TOKEN USAGE")
|
|
14323
|
+
lines.append(f" Input tokens: {fmt_number(total_input_tokens)}")
|
|
14324
|
+
lines.append(f" Output tokens: {fmt_number(total_output_tokens)}")
|
|
14325
|
+
lines.append(f" Estimated cost: ${total_cost:.2f}")
|
|
14326
|
+
lines.append("")
|
|
14327
|
+
lines.append("TIME SAVED")
|
|
14328
|
+
lines.append(f" Estimated: {time_saved_hours} hours")
|
|
14329
|
+
lines.append(f" (based on {fmt_number(total_iterations)} iterations x 15min each)")
|
|
14330
|
+
lines.append("")
|
|
14331
|
+
if episodic_count > 0 or semantic_count > 0:
|
|
14332
|
+
lines.append("MEMORY")
|
|
14333
|
+
lines.append(f" Episodic entries: {fmt_number(episodic_count)}")
|
|
14334
|
+
lines.append(f" Semantic patterns: {fmt_number(semantic_count)}")
|
|
14335
|
+
lines.append("")
|
|
14336
|
+
lines.append(f"Generated by Loki Mode v{version} | autonomi.dev")
|
|
14337
|
+
lines.append("Share your stats: loki metrics --share")
|
|
14338
|
+
lines.append("")
|
|
14339
|
+
|
|
14340
|
+
output_text = "\n".join(lines)
|
|
14341
|
+
print(output_text)
|
|
14342
|
+
|
|
14343
|
+
# Save to METRICS.md if requested
|
|
14344
|
+
if save_flag:
|
|
14345
|
+
try:
|
|
14346
|
+
with open("METRICS.md", "w") as f:
|
|
14347
|
+
f.write("# Loki Mode Productivity Report\n\n")
|
|
14348
|
+
f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
14349
|
+
f.write("```\n")
|
|
14350
|
+
f.write(output_text.strip())
|
|
14351
|
+
f.write("\n```\n")
|
|
14352
|
+
print("Saved to METRICS.md")
|
|
14353
|
+
except Exception as e:
|
|
14354
|
+
print(f"Error saving METRICS.md: {e}", file=sys.stderr)
|
|
14355
|
+
|
|
14356
|
+
METRICS_SCRIPT
|
|
14357
|
+
)
|
|
14358
|
+
local metrics_exit=$?
|
|
14359
|
+
|
|
14360
|
+
if [ $metrics_exit -ne 0 ]; then
|
|
14361
|
+
echo -e "${RED}Failed to generate metrics report${NC}"
|
|
14362
|
+
echo "$metrics_output"
|
|
14363
|
+
exit 1
|
|
14364
|
+
fi
|
|
14365
|
+
|
|
14366
|
+
echo "$metrics_output"
|
|
14367
|
+
|
|
14368
|
+
# Handle --share flag
|
|
14369
|
+
if [ "$share_flag" = true ]; then
|
|
14370
|
+
local tmpfile
|
|
14371
|
+
tmpfile=$(mktemp "/tmp/loki-metrics-XXXXXX.md")
|
|
14372
|
+
echo "$metrics_output" > "$tmpfile"
|
|
14373
|
+
|
|
14374
|
+
if ! command -v gh &>/dev/null; then
|
|
14375
|
+
echo -e "${RED}gh CLI not found -- cannot share${NC}"
|
|
14376
|
+
echo "Install: brew install gh"
|
|
14377
|
+
rm -f "$tmpfile"
|
|
13874
14378
|
exit 1
|
|
13875
|
-
|
|
13876
|
-
|
|
14379
|
+
fi
|
|
14380
|
+
|
|
14381
|
+
if ! gh auth status &>/dev/null 2>&1; then
|
|
14382
|
+
echo -e "${RED}GitHub CLI not authenticated${NC}"
|
|
14383
|
+
echo "Run 'gh auth login' to authenticate."
|
|
14384
|
+
rm -f "$tmpfile"
|
|
14385
|
+
exit 1
|
|
14386
|
+
fi
|
|
14387
|
+
|
|
14388
|
+
echo "Uploading metrics report..."
|
|
14389
|
+
local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
|
|
14390
|
+
local project_name
|
|
14391
|
+
project_name=$(basename "$(pwd)")
|
|
14392
|
+
local gist_url
|
|
14393
|
+
gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" --public 2>&1)
|
|
14394
|
+
local gist_exit=$?
|
|
14395
|
+
rm -f "$tmpfile"
|
|
14396
|
+
|
|
14397
|
+
if [ $gist_exit -ne 0 ]; then
|
|
14398
|
+
echo -e "${RED}Failed to create gist${NC}"
|
|
14399
|
+
echo "$gist_url"
|
|
14400
|
+
exit 1
|
|
14401
|
+
fi
|
|
14402
|
+
echo -e "${GREEN}Shared: ${gist_url}${NC}"
|
|
14403
|
+
fi
|
|
13877
14404
|
}
|
|
13878
14405
|
|
|
13879
14406
|
# Output shell completion scripts
|
package/bin/postinstall.js
CHANGED
|
@@ -85,6 +85,30 @@ if (results.some(r => !r.ok)) {
|
|
|
85
85
|
console.log(` loki setup-skill`);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// PATH check: warn if npm global bin is not in PATH
|
|
89
|
+
try {
|
|
90
|
+
const { execSync } = require('child_process');
|
|
91
|
+
const npmBin = execSync('npm bin -g 2>/dev/null || npm prefix -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
92
|
+
const npmBinDir = npmBin.endsWith('/bin') ? npmBin : npmBin + '/bin';
|
|
93
|
+
const pathDirs = (process.env.PATH || '').split(':');
|
|
94
|
+
const lokiBinInPath = pathDirs.some(d => d === npmBinDir || d === npmBin);
|
|
95
|
+
if (!lokiBinInPath) {
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('[IMPORTANT] The `loki` command may not be in your PATH.');
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log('Add the npm global bin directory to your PATH:');
|
|
100
|
+
console.log(` export PATH="${npmBinDir}:$PATH"`);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('To make this permanent, add it to your shell config (~/.zshrc or ~/.bashrc):');
|
|
103
|
+
console.log(` echo 'export PATH="${npmBinDir}:$PATH"' >> ~/.zshrc && source ~/.zshrc`);
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log('Or use the Homebrew tap (sets PATH automatically):');
|
|
106
|
+
console.log(' brew tap asklokesh/tap && brew install loki-mode');
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// If npm bin check fails, skip PATH warning silently
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
console.log('');
|
|
89
113
|
console.log('CLI commands:');
|
|
90
114
|
console.log(' loki start ./prd.md Start with Claude (default)');
|
package/completions/_loki
CHANGED
|
@@ -70,6 +70,15 @@ function _loki {
|
|
|
70
70
|
issue)
|
|
71
71
|
_loki_issue
|
|
72
72
|
;;
|
|
73
|
+
metrics)
|
|
74
|
+
_arguments \
|
|
75
|
+
'--json[Machine-readable JSON output]' \
|
|
76
|
+
'--last[Analyze last N sessions]:number:' \
|
|
77
|
+
'--save[Write report to METRICS.md]' \
|
|
78
|
+
'--share[Upload report as GitHub Gist]' \
|
|
79
|
+
'--help[Show help]' \
|
|
80
|
+
'1:subcommand:(prometheus)'
|
|
81
|
+
;;
|
|
73
82
|
share)
|
|
74
83
|
_arguments \
|
|
75
84
|
'--private[Create a secret gist]' \
|
|
@@ -115,6 +124,7 @@ function _loki_commands {
|
|
|
115
124
|
'voice:Voice input commands'
|
|
116
125
|
'doctor:Check system prerequisites'
|
|
117
126
|
'onboard:Analyze repo and generate CLAUDE.md'
|
|
127
|
+
'metrics:Session productivity report'
|
|
118
128
|
'share:Share session report as GitHub Gist'
|
|
119
129
|
'version:Show version'
|
|
120
130
|
'completions:Output shell completions'
|
package/completions/loki.bash
CHANGED
|
@@ -5,7 +5,7 @@ _loki_completion() {
|
|
|
5
5
|
_init_completion || return
|
|
6
6
|
|
|
7
7
|
# Main subcommands (must match autonomy/loki main case statement)
|
|
8
|
-
local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise voice secrets doctor watchdog audit metrics syslog onboard share version completions help"
|
|
8
|
+
local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise voice secrets doctor watchdog audit metrics syslog onboard share explain plan report test ci watch version completions help"
|
|
9
9
|
|
|
10
10
|
# 1. If we are on the first argument (subcommand)
|
|
11
11
|
if [[ $cword -eq 1 ]]; then
|
|
@@ -144,6 +144,24 @@ _loki_completion() {
|
|
|
144
144
|
_filedir -d
|
|
145
145
|
;;
|
|
146
146
|
|
|
147
|
+
metrics)
|
|
148
|
+
if [[ "$cur" == -* ]]; then
|
|
149
|
+
COMPREPLY=( $(compgen -W "--json --last --save --share --help" -- "$cur") )
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
local metrics_cmds="prometheus"
|
|
153
|
+
COMPREPLY=( $(compgen -W "${metrics_cmds}" -- "$cur") )
|
|
154
|
+
;;
|
|
155
|
+
|
|
156
|
+
watch)
|
|
157
|
+
if [[ "$cur" == -* ]]; then
|
|
158
|
+
COMPREPLY=( $(compgen -W "--once --interval --no-auto-start --debounce --help" -- "$cur") )
|
|
159
|
+
return 0
|
|
160
|
+
fi
|
|
161
|
+
# Default to file completion for PRD files
|
|
162
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
163
|
+
;;
|
|
164
|
+
|
|
147
165
|
share)
|
|
148
166
|
if [[ "$cur" == -* ]]; then
|
|
149
167
|
COMPREPLY=( $(compgen -W "--private --format --help" -- "$cur") )
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED