loki-mode 6.4.0 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/SKILL.md +8 -4
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +24 -0
- package/autonomy/council-v2.sh +14 -0
- package/autonomy/hooks/migration-hooks.sh +6 -2
- package/autonomy/loki +221 -67
- package/autonomy/run.sh +164 -22
- package/autonomy/sandbox.sh +48 -22
- package/autonomy/telemetry.sh +20 -7
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +1 -1
- package/dashboard/migration_engine.py +1 -1
- package/dashboard/server.py +27 -12
- package/docs/INSTALLATION.md +2 -2
- package/events/emit.sh +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +11 -6
- package/memory/embeddings.py +15 -6
- package/memory/engine.py +56 -0
- package/memory/schemas.py +6 -2
- package/memory/storage.py +5 -2
- package/memory/token_economics.py +4 -1
- package/package.json +1 -1
- package/providers/aider.sh +129 -0
- package/providers/cline.sh +122 -0
- package/providers/loader.sh +1 -1
- package/skills/model-selection.md +3 -1
- package/skills/providers.md +140 -17
package/events/emit.sh
CHANGED
|
@@ -43,7 +43,7 @@ fi
|
|
|
43
43
|
|
|
44
44
|
# JSON escape helper: handles \, ", and control characters
|
|
45
45
|
json_escape() {
|
|
46
|
-
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' |
|
|
46
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | awk '{if(NR>1) printf "\\n"; printf "%s", $0}'
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
# Build payload JSON
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -20,6 +20,7 @@ import os
|
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
22
|
import threading
|
|
23
|
+
import uuid
|
|
23
24
|
from datetime import datetime, timezone
|
|
24
25
|
from typing import Optional, List, Dict, Any
|
|
25
26
|
|
|
@@ -563,11 +564,11 @@ async def loki_memory_store_pattern(
|
|
|
563
564
|
from memory.schemas import SemanticPattern
|
|
564
565
|
|
|
565
566
|
base_path = safe_path_join('.loki', 'memory')
|
|
566
|
-
engine = MemoryEngine(base_path)
|
|
567
|
+
engine = MemoryEngine(base_path=base_path)
|
|
567
568
|
engine.initialize()
|
|
568
569
|
|
|
569
570
|
pattern_obj = SemanticPattern(
|
|
570
|
-
id=f"pattern-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}",
|
|
571
|
+
id=f"pattern-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:8]}",
|
|
571
572
|
pattern=pattern,
|
|
572
573
|
category=category,
|
|
573
574
|
conditions=[],
|
|
@@ -821,7 +822,7 @@ async def loki_state_get() -> str:
|
|
|
821
822
|
try:
|
|
822
823
|
from memory.engine import MemoryEngine
|
|
823
824
|
memory_path = safe_path_join('.loki', 'memory')
|
|
824
|
-
engine = MemoryEngine(memory_path)
|
|
825
|
+
engine = MemoryEngine(base_path=memory_path)
|
|
825
826
|
state["memory_stats"] = engine.get_stats()
|
|
826
827
|
except Exception:
|
|
827
828
|
state["memory_stats"] = None
|
|
@@ -1004,9 +1005,13 @@ async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
|
|
|
1004
1005
|
try:
|
|
1005
1006
|
content = prd_content
|
|
1006
1007
|
if not content and prd_path:
|
|
1007
|
-
|
|
1008
|
-
if os.path.
|
|
1009
|
-
|
|
1008
|
+
# Resolve relative paths against project root, absolute paths used as-is
|
|
1009
|
+
if os.path.isabs(prd_path):
|
|
1010
|
+
resolved = os.path.realpath(prd_path)
|
|
1011
|
+
else:
|
|
1012
|
+
resolved = os.path.realpath(os.path.join(get_project_root(), prd_path))
|
|
1013
|
+
if os.path.exists(resolved) and os.path.isfile(resolved):
|
|
1014
|
+
with open(resolved, 'r', encoding='utf-8') as f:
|
|
1010
1015
|
content = f.read()
|
|
1011
1016
|
else:
|
|
1012
1017
|
return json.dumps({"error": f"PRD file not found: {prd_path}"})
|
package/memory/embeddings.py
CHANGED
|
@@ -307,6 +307,10 @@ class TextChunker:
|
|
|
307
307
|
if len(text) <= max_size:
|
|
308
308
|
return [text]
|
|
309
309
|
|
|
310
|
+
# Guard against infinite loop when overlap >= max_size
|
|
311
|
+
if overlap >= max_size:
|
|
312
|
+
overlap = 0
|
|
313
|
+
|
|
310
314
|
chunks = []
|
|
311
315
|
start = 0
|
|
312
316
|
while start < len(text):
|
|
@@ -1062,12 +1066,17 @@ class EmbeddingEngine:
|
|
|
1062
1066
|
# Find texts that need computing
|
|
1063
1067
|
texts_to_compute = []
|
|
1064
1068
|
indices_to_compute = []
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1069
|
+
if not self.config.cache_enabled:
|
|
1070
|
+
# No cache - all texts need computing
|
|
1071
|
+
texts_to_compute = list(texts)
|
|
1072
|
+
indices_to_compute = list(range(len(texts)))
|
|
1073
|
+
else:
|
|
1074
|
+
for i, (text, key) in enumerate(zip(texts, cache_keys)):
|
|
1075
|
+
if cached_results.get(key) is None:
|
|
1076
|
+
texts_to_compute.append(text)
|
|
1077
|
+
indices_to_compute.append(i)
|
|
1078
|
+
else:
|
|
1079
|
+
self._metrics["cache_hits"] += 1
|
|
1071
1080
|
|
|
1072
1081
|
# Compute missing embeddings
|
|
1073
1082
|
new_embeddings = None
|
package/memory/engine.py
CHANGED
|
@@ -881,6 +881,21 @@ class MemoryEngine:
|
|
|
881
881
|
for e in errors_raw
|
|
882
882
|
]
|
|
883
883
|
|
|
884
|
+
# Parse last_accessed datetime
|
|
885
|
+
last_accessed = None
|
|
886
|
+
last_accessed_raw = data.get("last_accessed")
|
|
887
|
+
if last_accessed_raw:
|
|
888
|
+
if isinstance(last_accessed_raw, str):
|
|
889
|
+
if last_accessed_raw.endswith("Z"):
|
|
890
|
+
last_accessed_raw = last_accessed_raw[:-1]
|
|
891
|
+
last_accessed = datetime.fromisoformat(last_accessed_raw)
|
|
892
|
+
if last_accessed.tzinfo is None:
|
|
893
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
894
|
+
elif isinstance(last_accessed_raw, datetime):
|
|
895
|
+
last_accessed = last_accessed_raw
|
|
896
|
+
if last_accessed.tzinfo is None:
|
|
897
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
898
|
+
|
|
884
899
|
return EpisodeTrace(
|
|
885
900
|
id=data.get("id", ""),
|
|
886
901
|
task_id=data.get("task_id", ""),
|
|
@@ -897,6 +912,9 @@ class MemoryEngine:
|
|
|
897
912
|
tokens_used=data.get("tokens_used", 0),
|
|
898
913
|
files_read=data.get("files_read", context.get("files_involved", [])),
|
|
899
914
|
files_modified=data.get("files_modified", []),
|
|
915
|
+
importance=data.get("importance", 0.5),
|
|
916
|
+
last_accessed=last_accessed,
|
|
917
|
+
access_count=data.get("access_count", 0),
|
|
900
918
|
)
|
|
901
919
|
|
|
902
920
|
def _dict_to_pattern(self, data: Dict[str, Any]) -> SemanticPattern:
|
|
@@ -924,6 +942,21 @@ class MemoryEngine:
|
|
|
924
942
|
for link in links_raw
|
|
925
943
|
]
|
|
926
944
|
|
|
945
|
+
# Parse last_accessed datetime
|
|
946
|
+
last_accessed = None
|
|
947
|
+
last_accessed_raw = data.get("last_accessed")
|
|
948
|
+
if last_accessed_raw:
|
|
949
|
+
if isinstance(last_accessed_raw, str):
|
|
950
|
+
if last_accessed_raw.endswith("Z"):
|
|
951
|
+
last_accessed_raw = last_accessed_raw[:-1]
|
|
952
|
+
last_accessed = datetime.fromisoformat(last_accessed_raw)
|
|
953
|
+
if last_accessed.tzinfo is None:
|
|
954
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
955
|
+
elif isinstance(last_accessed_raw, datetime):
|
|
956
|
+
last_accessed = last_accessed_raw
|
|
957
|
+
if last_accessed.tzinfo is None:
|
|
958
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
959
|
+
|
|
927
960
|
return SemanticPattern(
|
|
928
961
|
id=data.get("id", ""),
|
|
929
962
|
pattern=data.get("pattern", ""),
|
|
@@ -936,6 +969,9 @@ class MemoryEngine:
|
|
|
936
969
|
usage_count=data.get("usage_count", 0),
|
|
937
970
|
last_used=last_used,
|
|
938
971
|
links=links,
|
|
972
|
+
importance=data.get("importance", 0.5),
|
|
973
|
+
last_accessed=last_accessed,
|
|
974
|
+
access_count=data.get("access_count", 0),
|
|
939
975
|
)
|
|
940
976
|
|
|
941
977
|
def _dict_to_skill(self, data: Dict[str, Any]) -> ProceduralSkill:
|
|
@@ -945,6 +981,22 @@ class MemoryEngine:
|
|
|
945
981
|
ErrorFix.from_dict(e) if isinstance(e, dict) else e
|
|
946
982
|
for e in raw_errors
|
|
947
983
|
]
|
|
984
|
+
|
|
985
|
+
# Parse last_accessed datetime
|
|
986
|
+
last_accessed = None
|
|
987
|
+
last_accessed_raw = data.get("last_accessed")
|
|
988
|
+
if last_accessed_raw:
|
|
989
|
+
if isinstance(last_accessed_raw, str):
|
|
990
|
+
if last_accessed_raw.endswith("Z"):
|
|
991
|
+
last_accessed_raw = last_accessed_raw[:-1]
|
|
992
|
+
last_accessed = datetime.fromisoformat(last_accessed_raw)
|
|
993
|
+
if last_accessed.tzinfo is None:
|
|
994
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
995
|
+
elif isinstance(last_accessed_raw, datetime):
|
|
996
|
+
last_accessed = last_accessed_raw
|
|
997
|
+
if last_accessed.tzinfo is None:
|
|
998
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
999
|
+
|
|
948
1000
|
return ProceduralSkill(
|
|
949
1001
|
id=data.get("id", ""),
|
|
950
1002
|
name=data.get("name", ""),
|
|
@@ -953,6 +1005,10 @@ class MemoryEngine:
|
|
|
953
1005
|
steps=data.get("steps", []),
|
|
954
1006
|
common_errors=common_errors,
|
|
955
1007
|
exit_criteria=data.get("exit_criteria", []),
|
|
1008
|
+
example_usage=data.get("example_usage"),
|
|
1009
|
+
importance=data.get("importance", 0.5),
|
|
1010
|
+
last_accessed=last_accessed,
|
|
1011
|
+
access_count=data.get("access_count", 0),
|
|
956
1012
|
)
|
|
957
1013
|
|
|
958
1014
|
def _skill_to_markdown(self, skill: Dict[str, Any]) -> str:
|
package/memory/schemas.py
CHANGED
|
@@ -20,9 +20,13 @@ from typing import Optional, List, Dict, Any
|
|
|
20
20
|
def _to_utc_isoformat(dt: datetime) -> str:
|
|
21
21
|
"""Convert datetime to UTC ISO 8601 string with Z suffix.
|
|
22
22
|
|
|
23
|
-
Handles both timezone-aware and timezone-naive datetimes
|
|
24
|
-
|
|
23
|
+
Handles both timezone-aware and timezone-naive datetimes.
|
|
24
|
+
If dt has a non-UTC timezone, converts to UTC first.
|
|
25
25
|
"""
|
|
26
|
+
# If timezone-aware and not UTC, convert to UTC
|
|
27
|
+
if dt.tzinfo is not None and dt.utcoffset() != timezone.utc.utcoffset(None):
|
|
28
|
+
dt = dt.astimezone(timezone.utc)
|
|
29
|
+
|
|
26
30
|
iso = dt.isoformat()
|
|
27
31
|
# If already has timezone offset like +00:00, replace with Z
|
|
28
32
|
if iso.endswith("+00:00"):
|
package/memory/storage.py
CHANGED
|
@@ -233,7 +233,7 @@ class MemoryStorage:
|
|
|
233
233
|
path: Path to JSON file
|
|
234
234
|
|
|
235
235
|
Returns:
|
|
236
|
-
Parsed JSON as dictionary, or None if file doesn't exist
|
|
236
|
+
Parsed JSON as dictionary, or None if file doesn't exist or is corrupted
|
|
237
237
|
"""
|
|
238
238
|
path = Path(path)
|
|
239
239
|
if not path.exists():
|
|
@@ -241,7 +241,10 @@ class MemoryStorage:
|
|
|
241
241
|
|
|
242
242
|
with self._file_lock(path, exclusive=False):
|
|
243
243
|
with open(path, "r") as f:
|
|
244
|
-
|
|
244
|
+
try:
|
|
245
|
+
return json.load(f)
|
|
246
|
+
except json.JSONDecodeError:
|
|
247
|
+
return None
|
|
245
248
|
|
|
246
249
|
def _generate_id(self, prefix: str) -> str:
|
|
247
250
|
"""
|
|
@@ -505,9 +505,12 @@ class TokenEconomics:
|
|
|
505
505
|
Ratio of discovery_tokens / read_tokens (0.0 if no reads)
|
|
506
506
|
"""
|
|
507
507
|
read_tokens = self.metrics["read_tokens"]
|
|
508
|
+
discovery_tokens = self.metrics["discovery_tokens"]
|
|
508
509
|
if read_tokens == 0:
|
|
510
|
+
if discovery_tokens > 0:
|
|
511
|
+
return 999.99 # Sentinel: all discovery, no productive reads
|
|
509
512
|
return 0.0
|
|
510
|
-
return
|
|
513
|
+
return discovery_tokens / read_tokens
|
|
511
514
|
|
|
512
515
|
def get_savings_percent(self) -> float:
|
|
513
516
|
"""
|
package/package.json
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Aider Provider Configuration (18+ Providers, Tier 3)
|
|
3
|
+
# Shell-sourceable config for loki-mode multi-provider support
|
|
4
|
+
|
|
5
|
+
# Provider Functions (for external use)
|
|
6
|
+
# =====================================
|
|
7
|
+
# These functions provide a clean interface for external scripts:
|
|
8
|
+
# provider_detect() - Check if CLI is installed
|
|
9
|
+
# provider_version() - Get CLI version
|
|
10
|
+
# provider_invoke() - Invoke with prompt (autonomous mode)
|
|
11
|
+
# provider_invoke_with_tier() - Invoke with tier-specific model selection
|
|
12
|
+
# provider_get_tier_param() - Map tier name to model name
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# source providers/aider.sh
|
|
16
|
+
# if provider_detect; then
|
|
17
|
+
# provider_invoke "Your prompt here"
|
|
18
|
+
# fi
|
|
19
|
+
#
|
|
20
|
+
# Note: autonomy/run.sh uses inline invocation for streaming support
|
|
21
|
+
# and real-time agent tracking. These functions are intended for
|
|
22
|
+
# simpler scripts, wrappers, and external integrations.
|
|
23
|
+
# =====================================
|
|
24
|
+
|
|
25
|
+
# Provider Identity
|
|
26
|
+
PROVIDER_NAME="aider"
|
|
27
|
+
PROVIDER_DISPLAY_NAME="Aider (18+ Providers)"
|
|
28
|
+
PROVIDER_CLI="aider"
|
|
29
|
+
|
|
30
|
+
# CLI Invocation
|
|
31
|
+
# Aider uses --message for single-instruction mode, --yes-always for auto-approve
|
|
32
|
+
# Prompt is passed via --message flag, not positional
|
|
33
|
+
PROVIDER_AUTONOMOUS_FLAG="--yes-always"
|
|
34
|
+
PROVIDER_PROMPT_FLAG="--message"
|
|
35
|
+
PROVIDER_PROMPT_POSITIONAL=false
|
|
36
|
+
|
|
37
|
+
# Skill System
|
|
38
|
+
# Aider does not have a native skills directory
|
|
39
|
+
PROVIDER_SKILL_DIR=""
|
|
40
|
+
PROVIDER_SKILL_FORMAT="none"
|
|
41
|
+
|
|
42
|
+
# Capability Flags
|
|
43
|
+
PROVIDER_HAS_SUBAGENTS=false
|
|
44
|
+
PROVIDER_HAS_PARALLEL=false
|
|
45
|
+
PROVIDER_HAS_TASK_TOOL=false
|
|
46
|
+
PROVIDER_HAS_MCP=false
|
|
47
|
+
PROVIDER_MAX_PARALLEL=1
|
|
48
|
+
|
|
49
|
+
# Model Configuration
|
|
50
|
+
# Aider supports 18+ providers; model configured via LOKI_AIDER_MODEL env var
|
|
51
|
+
# or provider-specific env vars (OPENAI_API_KEY, OPENAI_API_BASE, etc.)
|
|
52
|
+
PROVIDER_MODEL_PLANNING="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
53
|
+
PROVIDER_MODEL_DEVELOPMENT="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
54
|
+
PROVIDER_MODEL_FAST="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
55
|
+
|
|
56
|
+
# No Task tool - model is configured externally
|
|
57
|
+
PROVIDER_TASK_MODEL_PARAM=""
|
|
58
|
+
PROVIDER_TASK_MODEL_VALUES=()
|
|
59
|
+
|
|
60
|
+
# Context and Limits (varies by underlying model, conservative defaults)
|
|
61
|
+
PROVIDER_CONTEXT_WINDOW=200000
|
|
62
|
+
PROVIDER_MAX_OUTPUT_TOKENS=128000
|
|
63
|
+
PROVIDER_RATE_LIMIT_RPM=60
|
|
64
|
+
|
|
65
|
+
# Cost (varies by underlying provider, not tracked by loki)
|
|
66
|
+
PROVIDER_COST_INPUT_PLANNING=0.003
|
|
67
|
+
PROVIDER_COST_OUTPUT_PLANNING=0.015
|
|
68
|
+
PROVIDER_COST_INPUT_DEV=0.003
|
|
69
|
+
PROVIDER_COST_OUTPUT_DEV=0.015
|
|
70
|
+
PROVIDER_COST_INPUT_FAST=0.003
|
|
71
|
+
PROVIDER_COST_OUTPUT_FAST=0.015
|
|
72
|
+
|
|
73
|
+
# Degraded Mode
|
|
74
|
+
PROVIDER_DEGRADED=true
|
|
75
|
+
PROVIDER_DEGRADED_REASONS=(
|
|
76
|
+
"No subagent support"
|
|
77
|
+
"Sequential execution only"
|
|
78
|
+
"No Task tool or MCP"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Detection function - check if provider CLI is available
|
|
82
|
+
provider_detect() {
|
|
83
|
+
command -v aider >/dev/null 2>&1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Version check function
|
|
87
|
+
provider_version() {
|
|
88
|
+
aider --version 2>/dev/null | head -1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Invocation function
|
|
92
|
+
# --message: single instruction mode (process and exit)
|
|
93
|
+
# --yes-always: auto-approve all prompts
|
|
94
|
+
# --no-auto-commits: loki manages git
|
|
95
|
+
provider_invoke() {
|
|
96
|
+
local prompt="$1"
|
|
97
|
+
shift
|
|
98
|
+
local model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
99
|
+
local extra_flags="${LOKI_AIDER_FLAGS:-}"
|
|
100
|
+
# shellcheck disable=SC2086
|
|
101
|
+
aider --message "$prompt" \
|
|
102
|
+
--yes-always \
|
|
103
|
+
--no-auto-commits \
|
|
104
|
+
--model "$model" \
|
|
105
|
+
$extra_flags "$@" 2>&1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Model tier to parameter (Aider uses single model, returns model name)
|
|
109
|
+
provider_get_tier_param() {
|
|
110
|
+
local tier="$1"
|
|
111
|
+
echo "${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Dynamic model resolution (v6.0.0)
|
|
115
|
+
# Aider uses a single externally-configured model, so tier resolution
|
|
116
|
+
# just returns the configured model name. maxTier has no effect.
|
|
117
|
+
resolve_model_for_tier() {
|
|
118
|
+
local tier="$1"
|
|
119
|
+
echo "${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Tier-aware invocation
|
|
123
|
+
# Aider uses a single model configured externally, tier has no effect
|
|
124
|
+
provider_invoke_with_tier() {
|
|
125
|
+
local tier="$1"
|
|
126
|
+
local prompt="$2"
|
|
127
|
+
shift 2
|
|
128
|
+
provider_invoke "$prompt" "$@"
|
|
129
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cline CLI Provider Configuration (Multi-Provider, Tier 2)
|
|
3
|
+
# Shell-sourceable config for loki-mode multi-provider support
|
|
4
|
+
|
|
5
|
+
# Provider Functions (for external use)
|
|
6
|
+
# =====================================
|
|
7
|
+
# These functions provide a clean interface for external scripts:
|
|
8
|
+
# provider_detect() - Check if CLI is installed
|
|
9
|
+
# provider_version() - Get CLI version
|
|
10
|
+
# provider_invoke() - Invoke with prompt (autonomous mode)
|
|
11
|
+
# provider_invoke_with_tier() - Invoke with tier-specific model selection
|
|
12
|
+
# provider_get_tier_param() - Map tier name to model name
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# source providers/cline.sh
|
|
16
|
+
# if provider_detect; then
|
|
17
|
+
# provider_invoke "Your prompt here"
|
|
18
|
+
# fi
|
|
19
|
+
#
|
|
20
|
+
# Note: autonomy/run.sh uses inline invocation for streaming support
|
|
21
|
+
# and real-time agent tracking. These functions are intended for
|
|
22
|
+
# simpler scripts, wrappers, and external integrations.
|
|
23
|
+
# =====================================
|
|
24
|
+
|
|
25
|
+
# Provider Identity
|
|
26
|
+
PROVIDER_NAME="cline"
|
|
27
|
+
PROVIDER_DISPLAY_NAME="Cline CLI (Multi-Provider)"
|
|
28
|
+
PROVIDER_CLI="cline"
|
|
29
|
+
|
|
30
|
+
# CLI Invocation
|
|
31
|
+
# Cline CLI v2.5+ uses -y / --yolo for autonomous mode
|
|
32
|
+
# Prompt is passed as positional argument
|
|
33
|
+
PROVIDER_AUTONOMOUS_FLAG="-y"
|
|
34
|
+
PROVIDER_PROMPT_FLAG=""
|
|
35
|
+
PROVIDER_PROMPT_POSITIONAL=true
|
|
36
|
+
|
|
37
|
+
# Skill System
|
|
38
|
+
# Cline does not have a native skills directory
|
|
39
|
+
PROVIDER_SKILL_DIR=""
|
|
40
|
+
PROVIDER_SKILL_FORMAT="none"
|
|
41
|
+
|
|
42
|
+
# Capability Flags
|
|
43
|
+
# Cline has subagents and MCP -- NOT fully degraded (Tier 2)
|
|
44
|
+
PROVIDER_HAS_SUBAGENTS=true
|
|
45
|
+
PROVIDER_HAS_PARALLEL=false
|
|
46
|
+
PROVIDER_HAS_TASK_TOOL=false
|
|
47
|
+
PROVIDER_HAS_MCP=true
|
|
48
|
+
PROVIDER_MAX_PARALLEL=1
|
|
49
|
+
|
|
50
|
+
# Model Configuration
|
|
51
|
+
# Cline supports 12+ providers; model configured via LOKI_CLINE_MODEL env var
|
|
52
|
+
# or `cline auth` one-time setup. Defaults are placeholders.
|
|
53
|
+
PROVIDER_MODEL_PLANNING="${LOKI_CLINE_MODEL:-claude-sonnet-4-5-20250929}"
|
|
54
|
+
PROVIDER_MODEL_DEVELOPMENT="${LOKI_CLINE_MODEL:-claude-sonnet-4-5-20250929}"
|
|
55
|
+
PROVIDER_MODEL_FAST="${LOKI_CLINE_MODEL:-claude-sonnet-4-5-20250929}"
|
|
56
|
+
|
|
57
|
+
# No Task tool - model is configured externally
|
|
58
|
+
PROVIDER_TASK_MODEL_PARAM=""
|
|
59
|
+
PROVIDER_TASK_MODEL_VALUES=()
|
|
60
|
+
|
|
61
|
+
# Context and Limits (varies by underlying provider, conservative defaults)
|
|
62
|
+
PROVIDER_CONTEXT_WINDOW=200000
|
|
63
|
+
PROVIDER_MAX_OUTPUT_TOKENS=128000
|
|
64
|
+
PROVIDER_RATE_LIMIT_RPM=60
|
|
65
|
+
|
|
66
|
+
# Cost (varies by underlying provider, not tracked by loki)
|
|
67
|
+
PROVIDER_COST_INPUT_PLANNING=0.003
|
|
68
|
+
PROVIDER_COST_OUTPUT_PLANNING=0.015
|
|
69
|
+
PROVIDER_COST_INPUT_DEV=0.003
|
|
70
|
+
PROVIDER_COST_OUTPUT_DEV=0.015
|
|
71
|
+
PROVIDER_COST_INPUT_FAST=0.003
|
|
72
|
+
PROVIDER_COST_OUTPUT_FAST=0.015
|
|
73
|
+
|
|
74
|
+
# Degraded Mode
|
|
75
|
+
# Cline is NOT degraded -- it has subagents and MCP (Tier 2: near-full)
|
|
76
|
+
PROVIDER_DEGRADED=false
|
|
77
|
+
PROVIDER_DEGRADED_REASONS=()
|
|
78
|
+
|
|
79
|
+
# Detection function - check if provider CLI is available
|
|
80
|
+
provider_detect() {
|
|
81
|
+
command -v cline >/dev/null 2>&1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Version check function
|
|
85
|
+
provider_version() {
|
|
86
|
+
cline --version 2>/dev/null | head -1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Invocation function
|
|
90
|
+
# Uses -y (YOLO) for autonomous mode, positional prompt
|
|
91
|
+
provider_invoke() {
|
|
92
|
+
local prompt="$1"
|
|
93
|
+
shift
|
|
94
|
+
local model="${LOKI_CLINE_MODEL:-}"
|
|
95
|
+
local model_flag=""
|
|
96
|
+
[[ -n "$model" ]] && model_flag="-m $model"
|
|
97
|
+
# shellcheck disable=SC2086
|
|
98
|
+
cline -y $model_flag "$prompt" "$@" 2>&1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Model tier to parameter (Cline uses single model, returns model name)
|
|
102
|
+
provider_get_tier_param() {
|
|
103
|
+
local tier="$1"
|
|
104
|
+
echo "${LOKI_CLINE_MODEL:-default}"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Dynamic model resolution (v6.0.0)
|
|
108
|
+
# Cline uses a single externally-configured model, so tier resolution
|
|
109
|
+
# just returns the configured model name. maxTier has no effect.
|
|
110
|
+
resolve_model_for_tier() {
|
|
111
|
+
local tier="$1"
|
|
112
|
+
echo "${LOKI_CLINE_MODEL:-default}"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Tier-aware invocation
|
|
116
|
+
# Cline uses a single model configured externally, tier has no effect
|
|
117
|
+
provider_invoke_with_tier() {
|
|
118
|
+
local tier="$1"
|
|
119
|
+
local prompt="$2"
|
|
120
|
+
shift 2
|
|
121
|
+
provider_invoke "$prompt" "$@"
|
|
122
|
+
}
|
package/providers/loader.sh
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
PROVIDERS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
6
|
|
|
7
7
|
# List of supported providers
|
|
8
|
-
SUPPORTED_PROVIDERS=("claude" "codex" "gemini")
|
|
8
|
+
SUPPORTED_PROVIDERS=("claude" "codex" "gemini" "cline" "aider")
|
|
9
9
|
|
|
10
10
|
# Default provider
|
|
11
11
|
DEFAULT_PROVIDER="claude"
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Multi-Provider Support (v5.0.0)
|
|
4
4
|
|
|
5
|
-
Loki Mode supports
|
|
5
|
+
Loki Mode supports five AI providers. Claude has full features; all others run in **degraded mode** (sequential execution only, no Task tool, no parallel agents).
|
|
6
6
|
|
|
7
7
|
| Provider | Full Features | Degraded | CLI Flag |
|
|
8
8
|
|----------|---------------|----------|----------|
|
|
9
9
|
| **Claude Code** | Yes | No | `--provider claude` (default) |
|
|
10
10
|
| **OpenAI Codex CLI** | No | Yes | `--provider codex` |
|
|
11
11
|
| **Google Gemini CLI** | No | Yes | `--provider gemini` |
|
|
12
|
+
| **Cline CLI** | No | Yes | `--provider cline` |
|
|
13
|
+
| **Aider** | No | Yes | `--provider aider` |
|
|
12
14
|
|
|
13
15
|
**Degraded mode limitations:**
|
|
14
16
|
- No Task tool (cannot spawn subagents)
|