kivi-cli 2.0.6__py3-none-any.whl

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.
Files changed (47) hide show
  1. kivi_cli-2.0.6.dist-info/METADATA +17 -0
  2. kivi_cli-2.0.6.dist-info/RECORD +47 -0
  3. kivi_cli-2.0.6.dist-info/WHEEL +5 -0
  4. kivi_cli-2.0.6.dist-info/entry_points.txt +2 -0
  5. kivi_cli-2.0.6.dist-info/top_level.txt +1 -0
  6. kiviai/__init__.py +0 -0
  7. kiviai/backend.py +1372 -0
  8. kiviai/index.html +6172 -0
  9. kiviai/services/__init__.py +0 -0
  10. kiviai/services/git_service.py +219 -0
  11. kiviai/services/monitor.py +97 -0
  12. kiviai/services/providers/__init__.py +0 -0
  13. kiviai/services/providers/base.py +131 -0
  14. kiviai/services/providers/claude.py +125 -0
  15. kiviai/services/providers/copilot.py +251 -0
  16. kiviai/services/providers/inhouse.py +130 -0
  17. kiviai/services/providers/kivi.py +108 -0
  18. kiviai/services/providers/model_costs.py +137 -0
  19. kiviai/services/providers/openai_agent.py +113 -0
  20. kiviai/services/providers/opencode.py +157 -0
  21. kiviai/services/providers/orchestrator_provider.py +163 -0
  22. kiviai/services/providers/registry.py +89 -0
  23. kiviai/services/providers/vllm.py +319 -0
  24. kiviai/services/scheduler.py +244 -0
  25. kiviai/services/tools/__init__.py +1 -0
  26. kiviai/services/tools/copilot_tools/__init__.py +92 -0
  27. kiviai/services/tools/copilot_tools/agent_orchestration/__init__.py +17 -0
  28. kiviai/services/tools/copilot_tools/agent_orchestration/tools.py +296 -0
  29. kiviai/services/tools/copilot_tools/bash_execution/__init__.py +20 -0
  30. kiviai/services/tools/copilot_tools/bash_execution/sessions.py +335 -0
  31. kiviai/services/tools/copilot_tools/code_search/__init__.py +10 -0
  32. kiviai/services/tools/copilot_tools/code_search/tools.py +137 -0
  33. kiviai/services/tools/copilot_tools/file_operations/__init__.py +12 -0
  34. kiviai/services/tools/copilot_tools/file_operations/tools.py +197 -0
  35. kiviai/services/tools/copilot_tools/markdown/__init__.py +41 -0
  36. kiviai/services/tools/copilot_tools/markdown/custom_markdownify.py +774 -0
  37. kiviai/services/tools/copilot_tools/markdown/mrkdwn_analysis.py +1155 -0
  38. kiviai/services/tools/copilot_tools/markdown/tools.py +357 -0
  39. kiviai/services/tools/copilot_tools/session_workflow/__init__.py +17 -0
  40. kiviai/services/tools/copilot_tools/session_workflow/tools.py +199 -0
  41. kiviai/services/tools/copilot_tools/web/__init__.py +37 -0
  42. kiviai/services/tools/copilot_tools/web/_fetcher.py +39 -0
  43. kiviai/services/tools/copilot_tools/web/_store.py +122 -0
  44. kiviai/services/tools/copilot_tools/web/fetch.py +20 -0
  45. kiviai/services/tools/copilot_tools/web/search.py +116 -0
  46. kiviai/services/tools/copilot_tools/web/store_tools.py +92 -0
  47. kiviai/services/tools/tool_manager.py +239 -0
@@ -0,0 +1,113 @@
1
+ """OpenAI Agent SDK provider — uses openai-agents package."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import os
7
+ from typing import AsyncIterator
8
+
9
+ from .base import BaseProvider, ProviderConfig, ProviderEvent, EventType
10
+
11
+
12
+ class OpenAIAgentProvider(BaseProvider):
13
+ name = "openai-agent"
14
+ display_name = "OpenAI Agent"
15
+ description = "OpenAI's agent SDK with built-in tool use, handoffs, and guardrails. Runs multi-step workflows with GPT models. Supports streaming and sub-agent delegation. Requires OPENAI_API_KEY."
16
+ supports_streaming = True
17
+ supports_tools = True
18
+ supports_sessions = False
19
+ supports_agents = True
20
+ available_models = [
21
+ "gpt-5.4", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini",
22
+ "gpt-4.1-nano", "o3", "o3-mini", "o4-mini",
23
+ ]
24
+
25
+ def __init__(self):
26
+ self._stop = False
27
+
28
+ async def run(self, prompt: str, config: ProviderConfig, history=None) -> AsyncIterator[ProviderEvent]:
29
+ self._stop = False
30
+
31
+ api_key = config.api_key or os.environ.get("OPENAI_API_KEY", "")
32
+ model = config.model or "gpt-4o"
33
+
34
+ if not api_key:
35
+ yield ProviderEvent(type=EventType.ERROR, content="OPENAI_API_KEY not set")
36
+ return
37
+
38
+ try:
39
+ from openai import AsyncOpenAI
40
+
41
+ client = AsyncOpenAI(api_key=api_key)
42
+
43
+ messages = []
44
+ if history:
45
+ for h in history:
46
+ if h.get("role") in ("user", "assistant", "system") and not h.get("metadata"):
47
+ messages.append({"role": h["role"], "content": h["content"]})
48
+ messages.append({"role": "user", "content": prompt})
49
+
50
+ stream = await client.chat.completions.create(
51
+ model=model,
52
+ messages=messages,
53
+ stream=True,
54
+ max_tokens=4096,
55
+ )
56
+
57
+ text_buf = ""
58
+ last_chunk = None
59
+ async for chunk in stream:
60
+ if self._stop:
61
+ yield ProviderEvent(type=EventType.TEXT, content="⏹ *stopped*")
62
+ yield ProviderEvent(type=EventType.DONE)
63
+ return
64
+
65
+ last_chunk = chunk
66
+ delta = chunk.choices[0].delta if chunk.choices else None
67
+ if delta and delta.content:
68
+ text_buf += delta.content
69
+ yield ProviderEvent(type=EventType.TEXT, content=delta.content)
70
+
71
+ if chunk.choices and chunk.choices[0].finish_reason:
72
+ break
73
+
74
+ except ImportError:
75
+ yield ProviderEvent(type=EventType.ERROR, content="openai package required: pip install openai")
76
+ return
77
+ except Exception as e:
78
+ yield ProviderEvent(type=EventType.ERROR, content=str(e))
79
+ return
80
+
81
+ # Emit cost event from usage metadata on last chunk
82
+ from .model_costs import estimate_cost
83
+ input_tokens = 0
84
+ output_tokens = 0
85
+ if last_chunk and hasattr(last_chunk, 'usage') and last_chunk.usage:
86
+ input_tokens = last_chunk.usage.prompt_tokens or 0
87
+ output_tokens = last_chunk.usage.completion_tokens or 0
88
+
89
+ usage = {
90
+ "input_tokens": input_tokens,
91
+ "output_tokens": output_tokens,
92
+ }
93
+ total_cost = estimate_cost(
94
+ model,
95
+ input_tokens=input_tokens,
96
+ output_tokens=output_tokens,
97
+ )
98
+ yield ProviderEvent(
99
+ type=EventType.COST,
100
+ metadata={
101
+ "total_cost_usd": total_cost,
102
+ "usage": usage,
103
+ },
104
+ )
105
+
106
+ yield ProviderEvent(type=EventType.DONE)
107
+
108
+ async def stop(self):
109
+ self._stop = True
110
+
111
+ async def health_check(self):
112
+ api_key = os.environ.get("OPENAI_API_KEY", "")
113
+ return {"status": "ok" if api_key else "no_api_key", "provider": self.name}
@@ -0,0 +1,157 @@
1
+ """OpenCode provider — wraps `opencode run --format json`.
2
+
3
+ Event types from opencode CLI:
4
+ step_start — new LLM turn begins
5
+ text — streaming text chunk
6
+ tool_use — tool announced + completed (state has input/output/error)
7
+ step_finish — step done with token/cost info
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ from typing import AsyncIterator
14
+
15
+ from .base import BaseProvider, ProviderConfig, ProviderEvent, EventType
16
+
17
+
18
+ class OpenCodeProvider(BaseProvider):
19
+ name = "opencode"
20
+ display_name = "OpenCode"
21
+ description = "Open-source agentic coding CLI. File editing, shell access, and step-by-step reasoning with tool use. Supports persistent sessions and multiple LLM backends."
22
+ supports_streaming = True
23
+ supports_tools = True
24
+ supports_sessions = True
25
+ available_models = [
26
+ # opencode/* are free-tier models served by OpenCode itself (no API key
27
+ # needed) — verified live via `opencode models`. This list drifts as
28
+ # OpenCode rotates free models in/out; re-run `opencode models` to refresh.
29
+ "opencode/big-pickle", "opencode/deepseek-v4-flash-free", "opencode/hy3-free",
30
+ "opencode/mimo-v2.5-free", "opencode/nemotron-3-ultra-free", "opencode/north-mini-code-free",
31
+ # Below require the user's own provider credentials (`opencode auth login`).
32
+ "anthropic/claude-sonnet-5", "anthropic/claude-opus-4-8", "anthropic/claude-haiku-4-5",
33
+ "openai/gpt-5.4", "openai/gpt-4.1", "openai/gpt-4.1-mini", "openai/o3", "openai/o4-mini",
34
+
35
+ ]
36
+
37
+ def __init__(self):
38
+ self._stop = False
39
+ self._proc = None
40
+
41
+ async def run(self, prompt: str, config: ProviderConfig, history=None) -> AsyncIterator[ProviderEvent]:
42
+ self._stop = False
43
+ cmd = ["opencode", "run", "--format", "json"]
44
+ if config.model:
45
+ cmd += ["--model", config.model]
46
+ if config.session_id:
47
+ cmd += ["--session", config.session_id]
48
+ if config.cwd:
49
+ cmd += ["--dir", config.cwd]
50
+ if config.extra.get("thinking"):
51
+ cmd += ["--thinking"]
52
+ if config.extra.get("fork"):
53
+ cmd += ["--fork"]
54
+
55
+ # File attachments via -f flag
56
+ for f in config.extra.get("files", []):
57
+ cmd += ["-f", f]
58
+
59
+ cmd += [prompt]
60
+
61
+ session_id = None
62
+ total_cost = 0.0
63
+ usage = {}
64
+
65
+ try:
66
+ self._proc = await asyncio.create_subprocess_exec(
67
+ *cmd,
68
+ stdout=asyncio.subprocess.PIPE,
69
+ stderr=asyncio.subprocess.PIPE,
70
+ stdin=asyncio.subprocess.DEVNULL,
71
+ )
72
+
73
+ async for line in self._proc.stdout:
74
+ if self._stop:
75
+ self._proc.terminate()
76
+ yield ProviderEvent(type=EventType.TEXT, content="⏹ *stopped*")
77
+ yield ProviderEvent(type=EventType.DONE)
78
+ return
79
+
80
+ line = line.decode().strip()
81
+ if not line or not line.startswith("{"):
82
+ continue
83
+ try:
84
+ msg = json.loads(line)
85
+ except Exception:
86
+ continue
87
+
88
+ session_id = msg.get("sessionID", session_id)
89
+ mtype = msg.get("type")
90
+ part = msg.get("part", {})
91
+
92
+ if mtype == "text":
93
+ yield ProviderEvent(type=EventType.TEXT, content=part.get("text", ""))
94
+
95
+ elif mtype == "error":
96
+ error_data = msg.get("error", {})
97
+ error_msg = error_data.get("data", {}).get("message", "") or error_data.get("name", "Unknown error")
98
+ yield ProviderEvent(type=EventType.ERROR, content=f"OpenCode error: {error_msg}")
99
+ return
100
+
101
+ elif mtype == "tool_use":
102
+ state = part.get("state", {})
103
+ tool_name = part.get("tool", "tool")
104
+ title = part.get("title", tool_name) or tool_name
105
+ args = json.dumps(state.get("input", {}), indent=2)
106
+ output = state.get("output", "") or state.get("error", "")
107
+ if isinstance(output, list):
108
+ output = "\n".join(c.get("text", str(c)) for c in output)
109
+ status = state.get("status", "")
110
+ is_error = status == "error" or bool(state.get("error"))
111
+
112
+ yield ProviderEvent(
113
+ type=EventType.TOOL_START,
114
+ metadata={"title": f"⚙ {title}", "args": args},
115
+ )
116
+ yield ProviderEvent(
117
+ type=EventType.TOOL_RESULT,
118
+ content=str(output),
119
+ metadata={"is_error": is_error},
120
+ )
121
+
122
+ elif mtype == "step_finish":
123
+ tokens = part.get("tokens", {})
124
+ cache = tokens.get("cache", {})
125
+ step_cost = part.get("cost", 0.0)
126
+ total_cost += step_cost
127
+ usage["input_tokens"] = usage.get("input_tokens", 0) + tokens.get("input", 0)
128
+ usage["output_tokens"] = usage.get("output_tokens", 0) + tokens.get("output", 0)
129
+ usage["cache_read_input_tokens"] = usage.get("cache_read_input_tokens", 0) + cache.get("read", 0)
130
+ usage["cache_creation_input_tokens"] = usage.get("cache_creation_input_tokens", 0) + cache.get("write", 0)
131
+
132
+ await self._proc.wait()
133
+ except Exception as e:
134
+ yield ProviderEvent(type=EventType.ERROR, content=str(e))
135
+ return
136
+
137
+ if session_id or total_cost:
138
+ yield ProviderEvent(
139
+ type=EventType.COST,
140
+ metadata={
141
+ "session_id": session_id,
142
+ "total_cost_usd": total_cost,
143
+ "usage": usage,
144
+ },
145
+ )
146
+
147
+ yield ProviderEvent(type=EventType.DONE)
148
+
149
+ async def stop(self):
150
+ self._stop = True
151
+ if self._proc:
152
+ self._proc.terminate()
153
+
154
+ async def health_check(self):
155
+ import shutil
156
+ has_bin = shutil.which("opencode") is not None
157
+ return {"status": "ok" if has_bin else "unavailable", "provider": self.name}
@@ -0,0 +1,163 @@
1
+ """Multi-agent orchestrator provider — supervisor pattern with parallel workers."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import re
7
+ from typing import AsyncIterator
8
+
9
+ from .base import BaseProvider, ProviderConfig, ProviderEvent, EventType
10
+
11
+
12
+ class OrchestratorProvider(BaseProvider):
13
+ name = "claudeagents"
14
+ display_name = "Multi-Agent Orchestrator"
15
+ description = "Supervisor-worker pattern for parallel execution. Breaks complex tasks into subtasks, runs them across multiple Claude agents simultaneously, then synthesizes results. Best for large refactors, multi-file changes, and research tasks."
16
+ supports_streaming = True
17
+ supports_tools = True
18
+ supports_sessions = False
19
+ supports_agents = True
20
+ available_models = ["claude-sonnet-4.6"]
21
+
22
+ def __init__(self):
23
+ self._stop = False
24
+
25
+ async def run(self, prompt: str, config: ProviderConfig, history=None) -> AsyncIterator[ProviderEvent]:
26
+ """Supervisor loop: plan → parallel workers → review → repeat."""
27
+ self._stop = False
28
+
29
+ # Import Claude provider for worker execution
30
+ from .claude import ClaudeProvider
31
+ from .registry import get_provider
32
+
33
+ cwd = config.cwd or "."
34
+ mode = config.mode or "bypassPermissions"
35
+
36
+ # Step 1: Plan subtasks using supervisor
37
+ yield ProviderEvent(
38
+ type=EventType.TOOL_START,
39
+ metadata={"title": "🎯 Supervisor", "args": "Planning subtasks…"},
40
+ )
41
+
42
+ plan_prompt = (
43
+ "You are a task supervisor. Break this into independent subtasks for parallel execution.\n"
44
+ f'Respond ONLY with a JSON array: [{{"id":"1","prompt":"...","model":"haiku"}}]\n'
45
+ f"Use 'haiku' for most tasks. Use 'sonnet' only for complex reasoning.\n\n"
46
+ f"User request: {prompt}\nWorking directory: {cwd}"
47
+ )
48
+
49
+ try:
50
+ supervisor = ClaudeProvider()
51
+ plan_config = ProviderConfig(mode=mode, cwd=cwd)
52
+ plan_text = ""
53
+ async for event in supervisor.run(plan_prompt, plan_config):
54
+ if event.type == EventType.TEXT:
55
+ plan_text += event.content
56
+ elif event.type == EventType.COST:
57
+ yield event
58
+
59
+ # Parse plan
60
+ plan_text = plan_text.strip()
61
+ if plan_text.startswith("```"):
62
+ plan_text = "\n".join(plan_text.split("\n")[1:])
63
+ if plan_text.endswith("```"):
64
+ plan_text = plan_text[:-3]
65
+
66
+ try:
67
+ parsed = json.loads(plan_text.strip())
68
+ except json.JSONDecodeError:
69
+ match = re.search(r'\[.*\]', plan_text, re.DOTALL)
70
+ if match:
71
+ parsed = json.loads(match.group())
72
+ else:
73
+ parsed = [{"id": "1", "prompt": prompt, "model": "haiku"}]
74
+
75
+ if isinstance(parsed, dict):
76
+ subtasks = parsed.get("tasks", [parsed])
77
+ else:
78
+ subtasks = parsed
79
+
80
+ yield ProviderEvent(
81
+ type=EventType.TOOL_RESULT,
82
+ content=f"Planned {len(subtasks)} subtask(s)",
83
+ metadata={"title": "🎯 Plan", "is_error": False},
84
+ )
85
+
86
+ # Step 2: Run workers in parallel
87
+ MODEL_MAP = {
88
+ "haiku": "claude-haiku-4-5-20251001",
89
+ "sonnet": "claude-sonnet-4-6",
90
+ }
91
+
92
+ for st in subtasks:
93
+ worker_id = st.get("id", "1")
94
+ worker_prompt = st.get("prompt", prompt)
95
+ model_key = st.get("model", "haiku")
96
+ model = MODEL_MAP.get(model_key, MODEL_MAP["haiku"])
97
+
98
+ yield ProviderEvent(
99
+ type=EventType.AGENT_GROUP,
100
+ metadata={
101
+ "agent_id": worker_id,
102
+ "agent_label": f"Agent {worker_id} ({model_key})",
103
+ "model": model,
104
+ "status": "running",
105
+ "children": [],
106
+ },
107
+ )
108
+
109
+ # Execute workers
110
+ async def run_worker(st):
111
+ worker_id = st.get("id", "1")
112
+ worker_prompt = st.get("prompt", prompt)
113
+ model_key = st.get("model", "haiku")
114
+ model = MODEL_MAP.get(model_key, MODEL_MAP["haiku"])
115
+
116
+ worker = ClaudeProvider()
117
+ worker_config = ProviderConfig(
118
+ mode=mode, cwd=cwd,
119
+ extra={"model_override": model},
120
+ )
121
+
122
+ result_text = ""
123
+ events = []
124
+ async for event in worker.run(worker_prompt, worker_config):
125
+ if event.type == EventType.TEXT:
126
+ result_text += event.content
127
+ events.append(event)
128
+
129
+ return {"id": worker_id, "text": result_text, "events": events, "model": model_key}
130
+
131
+ results = await asyncio.gather(*[run_worker(st) for st in subtasks])
132
+
133
+ # Update agent groups with results
134
+ for r in results:
135
+ yield ProviderEvent(
136
+ type=EventType.AGENT_GROUP,
137
+ metadata={
138
+ "agent_id": r["id"],
139
+ "agent_label": f"Agent {r['id']} ({r['model']})",
140
+ "model": r["model"],
141
+ "status": "done",
142
+ "children": [{"role": "assistant", "content": r["text"]}],
143
+ },
144
+ )
145
+
146
+ # Step 3: Summarize
147
+ summary_parts = []
148
+ for r in results:
149
+ summary_parts.append(f"Agent {r['id']}: {r['text'][:200]}")
150
+
151
+ yield ProviderEvent(
152
+ type=EventType.TEXT,
153
+ content=f"All {len(results)} agent(s) completed.\n\n" + "\n\n".join(summary_parts),
154
+ )
155
+
156
+ except Exception as e:
157
+ yield ProviderEvent(type=EventType.ERROR, content=str(e))
158
+ return
159
+
160
+ yield ProviderEvent(type=EventType.DONE)
161
+
162
+ async def stop(self):
163
+ self._stop = True
@@ -0,0 +1,89 @@
1
+ """
2
+ Provider registry — maps model names to provider classes.
3
+ Single source of truth for which providers are available.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import Type
9
+
10
+ from .base import BaseProvider, ProviderConfig
11
+
12
+ log = logging.getLogger("cc-ui.registry")
13
+
14
+ _PROVIDERS: dict[str, Type[BaseProvider]] = {}
15
+ _ALIASES: dict[str, str] = {}
16
+
17
+
18
+ def register(name: str, provider_cls: Type[BaseProvider], aliases: list[str] | None = None):
19
+ """Register a provider class under a canonical name."""
20
+ _PROVIDERS[name] = provider_cls
21
+ if aliases:
22
+ for alias in aliases:
23
+ _ALIASES[alias] = name
24
+
25
+
26
+ def get_provider(name: str) -> BaseProvider:
27
+ """Instantiate a provider by name or alias."""
28
+ canonical = _ALIASES.get(name, name)
29
+ cls = _PROVIDERS.get(canonical)
30
+ if cls is None:
31
+ available = list(_PROVIDERS.keys()) + list(_ALIASES.keys())
32
+ raise ValueError(f"Unknown provider '{name}'. Available: {available}")
33
+ return cls()
34
+
35
+
36
+ def list_providers() -> list[dict]:
37
+ """Return capabilities of all registered providers."""
38
+ seen = set()
39
+ result = []
40
+ for name, cls in _PROVIDERS.items():
41
+ if name not in seen:
42
+ seen.add(name)
43
+ instance = cls()
44
+ info = instance.get_capabilities()
45
+ info["aliases"] = [a for a, c in _ALIASES.items() if c == name]
46
+ result.append(info)
47
+ return result
48
+
49
+
50
+ async def health_check_all() -> dict:
51
+ """Run health checks on all providers."""
52
+ results = {}
53
+ for name, cls in _PROVIDERS.items():
54
+ try:
55
+ instance = cls()
56
+ results[name] = await instance.health_check()
57
+ except Exception as e:
58
+ results[name] = {"status": "error", "error": str(e)}
59
+ return results
60
+
61
+
62
+ def _auto_register():
63
+ """Auto-register all built-in providers."""
64
+ try:
65
+ from .claude import ClaudeProvider
66
+ register("claude", ClaudeProvider, aliases=["claude-code", "cc", "sonnet", "opus", "haiku"])
67
+ except ImportError as e:
68
+ log.debug("Claude provider not available: %s", e)
69
+
70
+ try:
71
+ from .opencode import OpenCodeProvider
72
+ register("opencode", OpenCodeProvider, aliases=["oc"])
73
+ except ImportError as e:
74
+ log.debug("OpenCode provider not available: %s", e)
75
+
76
+ try:
77
+ from .copilot import CopilotProvider
78
+ register("copilot", CopilotProvider, aliases=["gh-copilot"])
79
+ except ImportError as e:
80
+ log.debug("Copilot provider not available: %s", e)
81
+
82
+ try:
83
+ from .vllm import VLLMProvider
84
+ register("vllm", VLLMProvider, aliases=["local", "vllm-local"])
85
+ except ImportError as e:
86
+ log.debug("vLLM provider not available: %s", e)
87
+
88
+
89
+ _auto_register()