loki-mode 7.7.9 → 7.7.11
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 +96 -4
- package/autonomy/run.sh +68 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +159 -3
- package/docs/INSTALLATION.md +1 -1
- package/docs/plans/FORGE-AUTONOMOUS-QUEUE.md +312 -0
- package/docs/plans/ULTRAPLAN-FORGE-BAAS.md +612 -0
- package/loki-ts/dist/loki.js +2 -2
- 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: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.7.
|
|
6
|
+
# Loki Mode v7.7.11
|
|
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.11 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.11
|
package/autonomy/loki
CHANGED
|
@@ -1470,6 +1470,26 @@ cmd_start() {
|
|
|
1470
1470
|
# Record session start time for efficiency tracking
|
|
1471
1471
|
record_session_start
|
|
1472
1472
|
|
|
1473
|
+
# UT2-13: Write cli-provider marker when --provider was explicitly passed.
|
|
1474
|
+
# This lets cmd_status_json report provider_source="cli" for the lifetime
|
|
1475
|
+
# of this session (24-hour heuristic). Atomic write via .tmp + mv.
|
|
1476
|
+
# v7.7.11 (council fix): validate provider against canonical set BEFORE
|
|
1477
|
+
# writing so a typo like `--provider xyz` does not pollute status output
|
|
1478
|
+
# with bogus state for 24h. Also write current PID alongside so the reader
|
|
1479
|
+
# can drop the marker when the originating process is no longer alive
|
|
1480
|
+
# (SIGKILL / crash paths that skip our cleanup hooks).
|
|
1481
|
+
case "$provider" in
|
|
1482
|
+
claude|codex|cline|aider)
|
|
1483
|
+
mkdir -p "$LOKI_DIR/state" 2>/dev/null || true
|
|
1484
|
+
printf '%s:%s:%s\n' "$provider" "$(date +%s)" "$$" \
|
|
1485
|
+
> "$LOKI_DIR/state/.cli-provider.tmp" 2>/dev/null \
|
|
1486
|
+
&& mv "$LOKI_DIR/state/.cli-provider.tmp" \
|
|
1487
|
+
"$LOKI_DIR/state/cli-provider" 2>/dev/null || true
|
|
1488
|
+
;;
|
|
1489
|
+
'') : ;; # no --provider passed
|
|
1490
|
+
*) log_warn "UT2-13: skipped cli-provider marker (unknown provider '$provider')" ;;
|
|
1491
|
+
esac
|
|
1492
|
+
|
|
1473
1493
|
# Emit learning signal: user preference for provider selection (SYN-018)
|
|
1474
1494
|
if [ -n "$provider" ]; then
|
|
1475
1495
|
# User explicitly chose a provider
|
|
@@ -2272,6 +2292,41 @@ dashboard_port = sys.argv[3]
|
|
|
2272
2292
|
env_provider = sys.argv[4]
|
|
2273
2293
|
result = {}
|
|
2274
2294
|
|
|
2295
|
+
# UT2-13: Helper -- read cli-provider marker written by --provider flag dispatch.
|
|
2296
|
+
# v7.7.11 (council fix): file format is now '<provider>:<unix_ts>:<pid>' so we
|
|
2297
|
+
# can drop the marker when the originating process is gone (SIGKILL / crash
|
|
2298
|
+
# paths that bypassed our cleanup hooks). Also validates provider name against
|
|
2299
|
+
# canonical set so a typo writing the file can't pollute status output.
|
|
2300
|
+
# Legacy format '<provider>:<unix_ts>' still parses (no PID liveness check).
|
|
2301
|
+
# Returns (provider_name, True) when valid; (None, False) otherwise.
|
|
2302
|
+
def _read_cli_provider(loki_dir):
|
|
2303
|
+
cli_file = os.path.join(loki_dir, 'state', 'cli-provider')
|
|
2304
|
+
if not os.path.isfile(cli_file):
|
|
2305
|
+
return None, False
|
|
2306
|
+
try:
|
|
2307
|
+
content = open(cli_file).read().strip()
|
|
2308
|
+
parts = content.split(':')
|
|
2309
|
+
if len(parts) < 2:
|
|
2310
|
+
return None, False
|
|
2311
|
+
prov = parts[0]
|
|
2312
|
+
ts = int(parts[1])
|
|
2313
|
+
if prov not in ('claude', 'codex', 'cline', 'aider'):
|
|
2314
|
+
return None, False
|
|
2315
|
+
if time.time() - ts > 86400:
|
|
2316
|
+
return None, False
|
|
2317
|
+
# If a PID is present, drop the marker when that process is gone.
|
|
2318
|
+
if len(parts) >= 3:
|
|
2319
|
+
try:
|
|
2320
|
+
pid = int(parts[2])
|
|
2321
|
+
if pid > 0:
|
|
2322
|
+
os.kill(pid, 0)
|
|
2323
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
2324
|
+
# PID missing/dead/cross-uid -> treat as stale.
|
|
2325
|
+
return None, False
|
|
2326
|
+
return prov, True
|
|
2327
|
+
except Exception:
|
|
2328
|
+
return None, False
|
|
2329
|
+
|
|
2275
2330
|
# Version
|
|
2276
2331
|
version_file = os.path.join(skill_dir, 'VERSION')
|
|
2277
2332
|
if os.path.isfile(version_file):
|
|
@@ -2285,8 +2340,17 @@ if not os.path.isdir(loki_dir):
|
|
|
2285
2340
|
result['status'] = 'inactive'
|
|
2286
2341
|
result['phase'] = None
|
|
2287
2342
|
result['iteration'] = 0
|
|
2288
|
-
|
|
2289
|
-
|
|
2343
|
+
# UT2-13: CLI check first (inactive path -- loki_dir may not exist yet).
|
|
2344
|
+
_cli_prov, _cli_set = _read_cli_provider(loki_dir)
|
|
2345
|
+
if _cli_set:
|
|
2346
|
+
result['provider'] = _cli_prov
|
|
2347
|
+
result['provider_source'] = 'cli'
|
|
2348
|
+
elif os.environ.get('LOKI_PROVIDER', ''):
|
|
2349
|
+
result['provider'] = env_provider
|
|
2350
|
+
result['provider_source'] = 'env'
|
|
2351
|
+
else:
|
|
2352
|
+
result['provider'] = 'claude'
|
|
2353
|
+
result['provider_source'] = 'default'
|
|
2290
2354
|
result['dashboard_url'] = None
|
|
2291
2355
|
result['pid'] = None
|
|
2292
2356
|
result['elapsed_time'] = 0
|
|
@@ -2345,7 +2409,9 @@ else:
|
|
|
2345
2409
|
result['iteration'] = 0
|
|
2346
2410
|
|
|
2347
2411
|
# Provider + provider_source (v7.7.2 B-5 clarity: surface WHY this
|
|
2348
|
-
# value won the resolution race: 'saved'
|
|
2412
|
+
# value won the resolution race: 'cli' > 'saved' > 'env' > 'default').
|
|
2413
|
+
# UT2-13: cli source is checked FIRST -- overrides saved/env/default.
|
|
2414
|
+
_cli_prov, _cli_set = _read_cli_provider(loki_dir)
|
|
2349
2415
|
provider_file = os.path.join(loki_dir, 'state', 'provider')
|
|
2350
2416
|
if os.path.isfile(provider_file):
|
|
2351
2417
|
try:
|
|
@@ -2356,7 +2422,10 @@ if os.path.isfile(provider_file):
|
|
|
2356
2422
|
else:
|
|
2357
2423
|
saved = ''
|
|
2358
2424
|
env_set = os.environ.get('LOKI_PROVIDER', '') != ''
|
|
2359
|
-
if
|
|
2425
|
+
if _cli_set:
|
|
2426
|
+
result['provider'] = _cli_prov
|
|
2427
|
+
result['provider_source'] = 'cli'
|
|
2428
|
+
elif saved:
|
|
2360
2429
|
result['provider'] = saved
|
|
2361
2430
|
result['provider_source'] = 'saved'
|
|
2362
2431
|
elif env_set:
|
|
@@ -4736,6 +4805,18 @@ cmd_run() {
|
|
|
4736
4805
|
if [[ -n "${2:-}" ]]; then
|
|
4737
4806
|
provider_override="$2"
|
|
4738
4807
|
start_args+=("--provider" "$2")
|
|
4808
|
+
# UT2-13: Record CLI provider at parse time (atomic write).
|
|
4809
|
+
# v7.7.11 (council fix): validate against canonical set; include
|
|
4810
|
+
# PID so reader can drop the marker when originating process is gone.
|
|
4811
|
+
case "$2" in
|
|
4812
|
+
claude|codex|cline|aider)
|
|
4813
|
+
mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
|
|
4814
|
+
printf '%s:%s:%s\n' "$2" "$(date +%s)" "$$" \
|
|
4815
|
+
> "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
|
|
4816
|
+
&& mv "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" \
|
|
4817
|
+
"${LOKI_DIR:-.loki}/state/cli-provider" 2>/dev/null || true
|
|
4818
|
+
;;
|
|
4819
|
+
esac
|
|
4739
4820
|
shift 2
|
|
4740
4821
|
else
|
|
4741
4822
|
echo -e "${RED}--provider requires a value${NC}"
|
|
@@ -4745,6 +4826,17 @@ cmd_run() {
|
|
|
4745
4826
|
--provider=*)
|
|
4746
4827
|
provider_override="${1#*=}"
|
|
4747
4828
|
start_args+=("--provider" "$provider_override")
|
|
4829
|
+
# UT2-13: Record CLI provider at parse time (atomic write).
|
|
4830
|
+
# v7.7.11 (council fix): validate + include PID.
|
|
4831
|
+
case "$provider_override" in
|
|
4832
|
+
claude|codex|cline|aider)
|
|
4833
|
+
mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
|
|
4834
|
+
printf '%s:%s:%s\n' "$provider_override" "$(date +%s)" "$$" \
|
|
4835
|
+
> "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
|
|
4836
|
+
&& mv "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" \
|
|
4837
|
+
"${LOKI_DIR:-.loki}/state/cli-provider" 2>/dev/null || true
|
|
4838
|
+
;;
|
|
4839
|
+
esac
|
|
4748
4840
|
shift
|
|
4749
4841
|
;;
|
|
4750
4842
|
--parallel|--bg|--background|--simple|--complex|--no-dashboard|--sandbox|--no-plan)
|
package/autonomy/run.sh
CHANGED
|
@@ -8755,10 +8755,68 @@ _intelligent_usage_regen() {
|
|
|
8755
8755
|
_manifests="${_manifests}=== $f ===\n$(head -50 "$target_dir/$f" 2>/dev/null)\n\n"
|
|
8756
8756
|
fi
|
|
8757
8757
|
done
|
|
8758
|
+
# v7.7.10 F-3 fix: include entrypoint file content so the model can read
|
|
8759
|
+
# the ACTUAL port / host bindings instead of guessing from package.json
|
|
8760
|
+
# scripts (which often imply port 3000 by convention but server.js may
|
|
8761
|
+
# bind a different port like 3001). Without this the regen wrote
|
|
8762
|
+
# "curl http://localhost:3000" for projects where the server bound 3001.
|
|
8763
|
+
local _entrypoints=""
|
|
8764
|
+
local _ep_candidates=""
|
|
8765
|
+
# Detect entrypoint from package.json main field if present
|
|
8766
|
+
if [ -f "$target_dir/package.json" ]; then
|
|
8767
|
+
local _pkg_main
|
|
8768
|
+
_pkg_main=$(python3 -c "import json,sys; d=json.load(open('$target_dir/package.json'));print(d.get('main') or '')" 2>/dev/null)
|
|
8769
|
+
[ -n "$_pkg_main" ] && _ep_candidates="$_ep_candidates $_pkg_main"
|
|
8770
|
+
# Extract files referenced in `scripts.start` and `scripts.dev`
|
|
8771
|
+
local _pkg_scripts
|
|
8772
|
+
_pkg_scripts=$(python3 -c "import json,re,sys; d=json.load(open('$target_dir/package.json'));s=d.get('scripts',{});c=' '.join([s.get('start',''),s.get('dev','')]);[print(t) for t in re.findall(r'[\\w/.-]+\\.(?:js|mjs|cjs|ts|mts|cts|py)\\b',c)]" 2>/dev/null)
|
|
8773
|
+
[ -n "$_pkg_scripts" ] && _ep_candidates="$_ep_candidates $_pkg_scripts"
|
|
8774
|
+
fi
|
|
8775
|
+
# Fallback convention names for common stacks
|
|
8776
|
+
for _ep in server.js server.ts server.mjs index.js index.ts app.js app.ts \
|
|
8777
|
+
main.py app.py server.py manage.py wsgi.py asgi.py \
|
|
8778
|
+
main.go cmd/server/main.go src/main.rs src/index.ts dist/server.js \
|
|
8779
|
+
build/server.js; do
|
|
8780
|
+
_ep_candidates="$_ep_candidates $_ep"
|
|
8781
|
+
done
|
|
8782
|
+
# Read first 80 lines of up to 3 unique existing candidates, scrubbing
|
|
8783
|
+
# common secret-bearing lines before they ship to the haiku endpoint.
|
|
8784
|
+
# v7.7.10 privacy guard: replaces lines matching API_KEY/SECRET/PASSWORD/
|
|
8785
|
+
# TOKEN/PRIVATE_KEY/AUTH/CREDENTIAL/BEARER assignments with [REDACTED]
|
|
8786
|
+
# so default-on regen does not exfiltrate hardcoded secrets. Port-binding
|
|
8787
|
+
# lines (listen/run/ListenAndServe with numeric literals) are preserved.
|
|
8788
|
+
# Opt out entirely with LOKI_INTELLIGENT_USAGE_INCLUDE_SOURCE=0.
|
|
8789
|
+
local _include_source="${LOKI_INTELLIGENT_USAGE_INCLUDE_SOURCE:-1}"
|
|
8790
|
+
local _seen="" _count=0
|
|
8791
|
+
for _ep in $_ep_candidates; do
|
|
8792
|
+
# Skip duplicates and non-existent files
|
|
8793
|
+
case " $_seen " in *" $_ep "*) continue ;; esac
|
|
8794
|
+
_seen="$_seen $_ep"
|
|
8795
|
+
if [ -f "$target_dir/$_ep" ]; then
|
|
8796
|
+
local _ep_body
|
|
8797
|
+
if [ "$_include_source" = "0" ]; then
|
|
8798
|
+
_ep_body="(entrypoint source omitted: LOKI_INTELLIGENT_USAGE_INCLUDE_SOURCE=0)"
|
|
8799
|
+
else
|
|
8800
|
+
# Scrub: any line whose text contains a credential keyword
|
|
8801
|
+
# has its value (everything after the first `:` or `=`)
|
|
8802
|
+
# replaced with [REDACTED]. Then any literal high-entropy
|
|
8803
|
+
# token shape (stripe sk-, github ghp_/ghs_, slack xox, GCP
|
|
8804
|
+
# AIza, AWS AKIA) is replaced inline. Port-binding lines
|
|
8805
|
+
# (no credential keyword) pass through unchanged.
|
|
8806
|
+
_ep_body=$(head -80 "$target_dir/$_ep" 2>/dev/null \
|
|
8807
|
+
| sed -E \
|
|
8808
|
+
-e '/[Aa][Pp][Ii][_-]?[Kk][Ee][Yy]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Tt][Oo][Kk][Ee][Nn]|[Pp][Rr][Ii][Vv][Aa][Tt][Ee][_-]?[Kk][Ee][Yy]|[Cc][Rr][Ee][Dd][Ee][Nn][Tt][Ii][Aa][Ll]|[Bb][Ee][Aa][Rr][Ee][Rr]/ s/[:=].*$/= [REDACTED]/' \
|
|
8809
|
+
-e 's/(sk-[A-Za-z0-9_-]{16,}|pk_[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{16,}|ghs_[A-Za-z0-9]{16,}|xox[bpoa]-[A-Za-z0-9-]{16,}|AIza[A-Za-z0-9_-]{32,}|AKIA[A-Z0-9]{12,})/[REDACTED]/g')
|
|
8810
|
+
fi
|
|
8811
|
+
_entrypoints="${_entrypoints}=== Entrypoint: $_ep ===\n${_ep_body}\n\n"
|
|
8812
|
+
_count=$((_count + 1))
|
|
8813
|
+
[ "$_count" -ge 3 ] && break
|
|
8814
|
+
fi
|
|
8815
|
+
done
|
|
8758
8816
|
_commits=$(cd "$target_dir" && git log --oneline -10 2>/dev/null || true)
|
|
8759
8817
|
|
|
8760
8818
|
log_info "Regenerating USAGE.md from final project state (intelligent mode)..."
|
|
8761
|
-
local _ic_prompt="You are writing a USAGE.md for the project below. Detect the stack from the manifest files; emit a concise (under 100 lines) Markdown doc with sections: ## Prerequisites, ## Install, ## Start, ## Verify (2-3 copy-paste curl/browser/CLI commands with expected output), ## Stop. Use the ACTUAL command names from package.json scripts or pyproject entry points -- never generic placeholders. Output ONLY the Markdown body (no code-fence wrapper, no preamble).
|
|
8819
|
+
local _ic_prompt="You are writing a USAGE.md for the project below. Detect the stack from the manifest files; emit a concise (under 100 lines) Markdown doc with sections: ## Prerequisites, ## Install, ## Start, ## Verify (2-3 copy-paste curl/browser/CLI commands with expected output), ## Stop. Use the ACTUAL command names from package.json scripts or pyproject entry points -- never generic placeholders. For ports, read the ENTRYPOINT file contents below (server.listen / app.listen / app.run / http.ListenAndServe / uvicorn.run port arg) -- do NOT infer port from script names or convention. If the entrypoint reads from process.env.PORT with a literal default, use the literal default. Output ONLY the Markdown body (no code-fence wrapper, no preamble).
|
|
8762
8820
|
|
|
8763
8821
|
=== Project tree (max 30 files, 3 levels deep) ===
|
|
8764
8822
|
${_tree}
|
|
@@ -8766,6 +8824,9 @@ ${_tree}
|
|
|
8766
8824
|
=== Manifest files ===
|
|
8767
8825
|
${_manifests}
|
|
8768
8826
|
|
|
8827
|
+
=== Entrypoint file contents (port bindings live here, NOT in package.json) ===
|
|
8828
|
+
${_entrypoints}
|
|
8829
|
+
|
|
8769
8830
|
=== Last 10 commits ===
|
|
8770
8831
|
${_commits}"
|
|
8771
8832
|
|
|
@@ -12136,6 +12197,8 @@ cleanup() {
|
|
|
12136
12197
|
# v7.5.12: Kill any running provider pipeline first, before slow cleanup.
|
|
12137
12198
|
kill_provider_child 2>/dev/null || true
|
|
12138
12199
|
rm -f "$loki_dir/STOP" "$loki_dir/PAUSE" "$loki_dir/PAUSED.md" 2>/dev/null
|
|
12200
|
+
# UT2-13: Clear cli-provider marker on session end.
|
|
12201
|
+
rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
|
|
12139
12202
|
if type app_runner_cleanup &>/dev/null; then
|
|
12140
12203
|
app_runner_cleanup
|
|
12141
12204
|
fi
|
|
@@ -12183,6 +12246,8 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
12183
12246
|
stop_status_monitor
|
|
12184
12247
|
kill_all_registered
|
|
12185
12248
|
rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
|
|
12249
|
+
# UT2-13: Clear cli-provider marker on session end.
|
|
12250
|
+
rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
|
|
12186
12251
|
# Clean up per-session PID file if running with session ID
|
|
12187
12252
|
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
12188
12253
|
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
|
@@ -12789,6 +12854,8 @@ main() {
|
|
|
12789
12854
|
stop_status_monitor
|
|
12790
12855
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
12791
12856
|
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
12857
|
+
# UT2-13: Clear cli-provider marker on normal session end.
|
|
12858
|
+
rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
|
|
12792
12859
|
# Clean up per-session PID file if running with session ID
|
|
12793
12860
|
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
12794
12861
|
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
|
package/dashboard/__init__.py
CHANGED
|
@@ -492,6 +492,24 @@
|
|
|
492
492
|
font-size: 13px;
|
|
493
493
|
color: var(--loki-text-secondary);
|
|
494
494
|
}
|
|
495
|
+
|
|
496
|
+
/* USAGE.md markdown render styles */
|
|
497
|
+
.usage-md { max-height: 480px; overflow: auto; padding: 12px; background: var(--loki-bg-secondary, #111); border-radius: 4px; color: var(--loki-text-primary, #eee); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.6; }
|
|
498
|
+
.usage-md h1 { font-size: 1.35rem; font-weight: 600; margin: 0.75em 0 0.4em; border-bottom: 1px solid var(--loki-border, #333); padding-bottom: 4px; }
|
|
499
|
+
.usage-md h2 { font-size: 1.15rem; font-weight: 600; margin: 0.75em 0 0.4em; }
|
|
500
|
+
.usage-md h3 { font-size: 1rem; font-weight: 600; margin: 0.65em 0 0.3em; }
|
|
501
|
+
.usage-md p { margin: 0.4em 0; }
|
|
502
|
+
.usage-md ul, .usage-md ol { margin: 0.4em 0 0.4em 1.5em; padding: 0; }
|
|
503
|
+
.usage-md li { margin: 0.15em 0; }
|
|
504
|
+
.usage-md pre { background: var(--loki-bg-tertiary, #0a0a0a); border: 1px solid var(--loki-border, #333); border-radius: 4px; padding: 10px 12px; overflow-x: auto; margin: 0.5em 0; }
|
|
505
|
+
.usage-md pre code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 11.5px; background: none; padding: 0; white-space: pre; }
|
|
506
|
+
.usage-md code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 11.5px; background: var(--loki-bg-tertiary, #0a0a0a); padding: 1px 4px; border-radius: 3px; }
|
|
507
|
+
.usage-md blockquote { border-left: 3px solid var(--loki-accent, #553de9); margin: 0.5em 0; padding: 4px 12px; color: var(--loki-text-secondary, #888); }
|
|
508
|
+
.usage-md hr { border: none; border-top: 1px solid var(--loki-border, #333); margin: 0.75em 0; }
|
|
509
|
+
.usage-md strong { font-weight: 600; }
|
|
510
|
+
.usage-md em { font-style: italic; }
|
|
511
|
+
.usage-md a { color: var(--loki-accent, #553de9); text-decoration: none; }
|
|
512
|
+
.usage-md a:hover { text-decoration: underline; }
|
|
495
513
|
</style>
|
|
496
514
|
</head>
|
|
497
515
|
<body>
|
|
@@ -707,10 +725,148 @@
|
|
|
707
725
|
<h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">How to Run (USAGE.md)</h3>
|
|
708
726
|
<div id="usage-doc-panel" style="background: var(--loki-bg-card, #1a1a1a); border: 1px solid var(--loki-border, #333); border-radius: 5px; padding: 12px;">
|
|
709
727
|
<div id="usage-doc-meta" style="font-size: 11px; color: var(--loki-text-muted, #888); margin-bottom: 8px;">Loading...</div>
|
|
710
|
-
<
|
|
728
|
+
<div id="usage-doc-content" class="usage-md"></div>
|
|
711
729
|
</div>
|
|
712
730
|
<script>
|
|
713
731
|
(function(){
|
|
732
|
+
// Minimal markdown-to-HTML converter for USAGE.md content.
|
|
733
|
+
// Handles: fenced code blocks, headings, horizontal rules,
|
|
734
|
+
// blockquotes, unordered/ordered lists, inline bold/italic/code,
|
|
735
|
+
// and paragraphs. Script tags are stripped from non-code text.
|
|
736
|
+
function renderUsageMarkdown(md) {
|
|
737
|
+
if (!md) return '';
|
|
738
|
+
var lines = md.split('
|
|
739
|
+
');
|
|
740
|
+
var html = '';
|
|
741
|
+
var i = 0;
|
|
742
|
+
var inList = null; // 'ul' or 'ol'
|
|
743
|
+
|
|
744
|
+
function escapeHtml(s) {
|
|
745
|
+
return s.replace(/&/g, '&')
|
|
746
|
+
.replace(/</g, '<')
|
|
747
|
+
.replace(/>/g, '>')
|
|
748
|
+
.replace(/"/g, '"');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function closeList() {
|
|
752
|
+
if (inList) { html += '</' + inList + '>'; inList = null; }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function inlineFormat(s) {
|
|
756
|
+
// Escape HTML first (outside code spans)
|
|
757
|
+
// Process inline code spans before escaping the rest
|
|
758
|
+
var BT = '`'; // backtick -- avoid literal backtick inside template literal
|
|
759
|
+
var result = '';
|
|
760
|
+
var codeSpanRe = new RegExp('(' + BT + '[^' + BT + ']+' + BT + ')');
|
|
761
|
+
var chunks = s.split(codeSpanRe);
|
|
762
|
+
for (var ci = 0; ci < chunks.length; ci++) {
|
|
763
|
+
var ch = chunks[ci];
|
|
764
|
+
if (ch.charAt(0) === BT && ch.charAt(ch.length - 1) === BT && ch.length > 2) {
|
|
765
|
+
result += '<code>' + escapeHtml(ch.slice(1, -1)) + '</code>';
|
|
766
|
+
} else {
|
|
767
|
+
var esc = escapeHtml(ch);
|
|
768
|
+
// strip any residual <script tags that survived escaping (defensive)
|
|
769
|
+
esc = esc.replace(/<script/gi, '<script');
|
|
770
|
+
esc = esc.replace(/**([^*]+)**/g, '<strong>$1</strong>');
|
|
771
|
+
esc = esc.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
772
|
+
esc = esc.replace(/*([^*]+)*/g, '<em>$1</em>');
|
|
773
|
+
esc = esc.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
774
|
+
// v7.7.11 XSS guard (Opus 1 council): only allow http/https/mailto/anchor/relative href.
|
|
775
|
+
// javascript: / data: / vbscript: are stripped to a plain code span so the URL stays visible
|
|
776
|
+
// but is not clickable. USAGE.md absorbs agent output + PRD text; treat as untrusted.
|
|
777
|
+
esc = esc.replace(/[([^]]+)](([^)]+))/g, function(_m, label, url){
|
|
778
|
+
var safe = /^(https?://|mailto:|#|/(?!/))/.test(url);
|
|
779
|
+
if (safe) return '<a href="' + url + '" rel="noopener noreferrer">' + label + '</a>';
|
|
780
|
+
return '<code>' + label + ' (' + url + ')</code>';
|
|
781
|
+
});
|
|
782
|
+
result += esc;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
while (i < lines.length) {
|
|
789
|
+
var line = lines[i];
|
|
790
|
+
|
|
791
|
+
// Fenced code block (BT3 = three backticks)
|
|
792
|
+
var BT3 = '```';
|
|
793
|
+
if (line.slice(0, 3) === BT3) {
|
|
794
|
+
closeList();
|
|
795
|
+
var lang = line.slice(3).trim();
|
|
796
|
+
var codeLines = [];
|
|
797
|
+
i++;
|
|
798
|
+
while (i < lines.length && lines[i].slice(0, 3) !== BT3) {
|
|
799
|
+
codeLines.push(lines[i]);
|
|
800
|
+
i++;
|
|
801
|
+
}
|
|
802
|
+
var codeContent = escapeHtml(codeLines.join('
|
|
803
|
+
'));
|
|
804
|
+
html += '<pre><code' + (lang ? ' class="language-' + escapeHtml(lang) + '"' : '') + '>' + codeContent + '</code></pre>';
|
|
805
|
+
i++;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Headings
|
|
810
|
+
var hMatch = line.match(/^(#{1,6})s+(.*)/);
|
|
811
|
+
if (hMatch) {
|
|
812
|
+
closeList();
|
|
813
|
+
var level = hMatch[1].length;
|
|
814
|
+
html += '<h' + level + '>' + inlineFormat(hMatch[2]) + '</h' + level + '>';
|
|
815
|
+
i++;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Horizontal rule
|
|
820
|
+
if (/^(-{3,}|*{3,}|_{3,})$/.test(line.trim())) {
|
|
821
|
+
closeList();
|
|
822
|
+
html += '<hr>';
|
|
823
|
+
i++;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Blockquote
|
|
828
|
+
if (/^>/.test(line)) {
|
|
829
|
+
closeList();
|
|
830
|
+
html += '<blockquote>' + inlineFormat(line.replace(/^>s?/, '')) + '</blockquote>';
|
|
831
|
+
i++;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Unordered list
|
|
836
|
+
var ulMatch = line.match(/^(s*[-*+])s+(.*)/);
|
|
837
|
+
if (ulMatch) {
|
|
838
|
+
if (inList !== 'ul') { closeList(); html += '<ul>'; inList = 'ul'; }
|
|
839
|
+
html += '<li>' + inlineFormat(ulMatch[2]) + '</li>';
|
|
840
|
+
i++;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Ordered list
|
|
845
|
+
var olMatch = line.match(/^s*d+.s+(.*)/);
|
|
846
|
+
if (olMatch) {
|
|
847
|
+
if (inList !== 'ol') { closeList(); html += '<ol>'; inList = 'ol'; }
|
|
848
|
+
html += '<li>' + inlineFormat(olMatch[1]) + '</li>';
|
|
849
|
+
i++;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Blank line
|
|
854
|
+
if (line.trim() === '') {
|
|
855
|
+
closeList();
|
|
856
|
+
i++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Paragraph
|
|
861
|
+
closeList();
|
|
862
|
+
html += '<p>' + inlineFormat(line) + '</p>';
|
|
863
|
+
i++;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
closeList();
|
|
867
|
+
return html;
|
|
868
|
+
}
|
|
869
|
+
|
|
714
870
|
function loadUsage(){
|
|
715
871
|
fetch('/api/usage').then(function(r){ return r.json(); }).then(function(j){
|
|
716
872
|
var meta = document.getElementById('usage-doc-meta');
|
|
@@ -718,13 +874,13 @@
|
|
|
718
874
|
if (!meta || !body) return;
|
|
719
875
|
if (!j || !j.exists){
|
|
720
876
|
meta.textContent = 'USAGE.md not generated yet. Loki writes it at the end of each session.';
|
|
721
|
-
body.
|
|
877
|
+
body.innerHTML = '';
|
|
722
878
|
return;
|
|
723
879
|
}
|
|
724
880
|
var size = j.size > 1024 ? (j.size/1024).toFixed(1) + ' KB' : j.size + ' B';
|
|
725
881
|
var when = j.mtime ? new Date(j.mtime*1000).toLocaleString() : '';
|
|
726
882
|
meta.textContent = j.path + ' (' + size + (j.truncated ? ', truncated' : '') + ', ' + when + ')';
|
|
727
|
-
body.
|
|
883
|
+
body.innerHTML = renderUsageMarkdown(j.content || '');
|
|
728
884
|
}).catch(function(e){
|
|
729
885
|
var meta = document.getElementById('usage-doc-meta');
|
|
730
886
|
if (meta) meta.textContent = 'Failed to load USAGE.md: ' + (e && e.message ? e.message : 'unknown');
|