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.
- kivi_cli-2.0.6.dist-info/METADATA +17 -0
- kivi_cli-2.0.6.dist-info/RECORD +47 -0
- kivi_cli-2.0.6.dist-info/WHEEL +5 -0
- kivi_cli-2.0.6.dist-info/entry_points.txt +2 -0
- kivi_cli-2.0.6.dist-info/top_level.txt +1 -0
- kiviai/__init__.py +0 -0
- kiviai/backend.py +1372 -0
- kiviai/index.html +6172 -0
- kiviai/services/__init__.py +0 -0
- kiviai/services/git_service.py +219 -0
- kiviai/services/monitor.py +97 -0
- kiviai/services/providers/__init__.py +0 -0
- kiviai/services/providers/base.py +131 -0
- kiviai/services/providers/claude.py +125 -0
- kiviai/services/providers/copilot.py +251 -0
- kiviai/services/providers/inhouse.py +130 -0
- kiviai/services/providers/kivi.py +108 -0
- kiviai/services/providers/model_costs.py +137 -0
- kiviai/services/providers/openai_agent.py +113 -0
- kiviai/services/providers/opencode.py +157 -0
- kiviai/services/providers/orchestrator_provider.py +163 -0
- kiviai/services/providers/registry.py +89 -0
- kiviai/services/providers/vllm.py +319 -0
- kiviai/services/scheduler.py +244 -0
- kiviai/services/tools/__init__.py +1 -0
- kiviai/services/tools/copilot_tools/__init__.py +92 -0
- kiviai/services/tools/copilot_tools/agent_orchestration/__init__.py +17 -0
- kiviai/services/tools/copilot_tools/agent_orchestration/tools.py +296 -0
- kiviai/services/tools/copilot_tools/bash_execution/__init__.py +20 -0
- kiviai/services/tools/copilot_tools/bash_execution/sessions.py +335 -0
- kiviai/services/tools/copilot_tools/code_search/__init__.py +10 -0
- kiviai/services/tools/copilot_tools/code_search/tools.py +137 -0
- kiviai/services/tools/copilot_tools/file_operations/__init__.py +12 -0
- kiviai/services/tools/copilot_tools/file_operations/tools.py +197 -0
- kiviai/services/tools/copilot_tools/markdown/__init__.py +41 -0
- kiviai/services/tools/copilot_tools/markdown/custom_markdownify.py +774 -0
- kiviai/services/tools/copilot_tools/markdown/mrkdwn_analysis.py +1155 -0
- kiviai/services/tools/copilot_tools/markdown/tools.py +357 -0
- kiviai/services/tools/copilot_tools/session_workflow/__init__.py +17 -0
- kiviai/services/tools/copilot_tools/session_workflow/tools.py +199 -0
- kiviai/services/tools/copilot_tools/web/__init__.py +37 -0
- kiviai/services/tools/copilot_tools/web/_fetcher.py +39 -0
- kiviai/services/tools/copilot_tools/web/_store.py +122 -0
- kiviai/services/tools/copilot_tools/web/fetch.py +20 -0
- kiviai/services/tools/copilot_tools/web/search.py +116 -0
- kiviai/services/tools/copilot_tools/web/store_tools.py +92 -0
- kiviai/services/tools/tool_manager.py +239 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code search primitives: grep (ripgrep wrapper) and glob_search.
|
|
3
|
+
|
|
4
|
+
Both are designed for speed-first, low-overhead discovery inside a repository.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Enums
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
class OutputMode(str, Enum):
|
|
20
|
+
FILES_WITH_MATCHES = "files_with_matches"
|
|
21
|
+
CONTENT = "content"
|
|
22
|
+
COUNT = "count"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# grep — ripgrep wrapper
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def grep(
|
|
30
|
+
pattern: str,
|
|
31
|
+
*,
|
|
32
|
+
path: Optional[str] = None,
|
|
33
|
+
output_mode: str = "files_with_matches",
|
|
34
|
+
glob_filter: Optional[str] = None,
|
|
35
|
+
file_type: Optional[str] = None,
|
|
36
|
+
case_insensitive: bool = False,
|
|
37
|
+
line_numbers: bool = False,
|
|
38
|
+
after_context: Optional[int] = None,
|
|
39
|
+
before_context: Optional[int] = None,
|
|
40
|
+
context: Optional[int] = None,
|
|
41
|
+
multiline: bool = False,
|
|
42
|
+
head_limit: Optional[int] = None,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Fast code search using **ripgrep** (``rg``).
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
pattern:
|
|
49
|
+
Regex pattern to search for.
|
|
50
|
+
path:
|
|
51
|
+
File or directory to search. Defaults to cwd.
|
|
52
|
+
output_mode:
|
|
53
|
+
``"files_with_matches"`` | ``"content"`` | ``"count"``.
|
|
54
|
+
glob_filter:
|
|
55
|
+
Glob to filter files (e.g. ``"*.py"``).
|
|
56
|
+
file_type:
|
|
57
|
+
Ripgrep type filter (e.g. ``"py"``, ``"js"``).
|
|
58
|
+
case_insensitive:
|
|
59
|
+
Enable ``-i`` flag.
|
|
60
|
+
line_numbers:
|
|
61
|
+
Show line numbers (requires ``output_mode="content"``).
|
|
62
|
+
after_context / before_context / context:
|
|
63
|
+
Context lines around matches (requires ``output_mode="content"``).
|
|
64
|
+
multiline:
|
|
65
|
+
Allow patterns that span multiple lines.
|
|
66
|
+
head_limit:
|
|
67
|
+
Max number of results.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
Ripgrep output as a string.
|
|
72
|
+
"""
|
|
73
|
+
mode = OutputMode(output_mode)
|
|
74
|
+
search_path = Path(path) if path else Path.cwd()
|
|
75
|
+
|
|
76
|
+
args: list[str] = ["rg"]
|
|
77
|
+
|
|
78
|
+
# Output mode flags
|
|
79
|
+
if mode == OutputMode.FILES_WITH_MATCHES:
|
|
80
|
+
args.append("--files-with-matches")
|
|
81
|
+
elif mode == OutputMode.COUNT:
|
|
82
|
+
args.append("--count")
|
|
83
|
+
# content mode uses default rg output
|
|
84
|
+
|
|
85
|
+
# Optional flags
|
|
86
|
+
if glob_filter:
|
|
87
|
+
args.extend(["--glob", glob_filter])
|
|
88
|
+
if file_type:
|
|
89
|
+
args.extend(["--type", file_type])
|
|
90
|
+
if case_insensitive:
|
|
91
|
+
args.append("--ignore-case")
|
|
92
|
+
if line_numbers and mode == OutputMode.CONTENT:
|
|
93
|
+
args.append("--line-number")
|
|
94
|
+
if after_context is not None and mode == OutputMode.CONTENT:
|
|
95
|
+
args.extend(["--after-context", str(after_context)])
|
|
96
|
+
if before_context is not None and mode == OutputMode.CONTENT:
|
|
97
|
+
args.extend(["--before-context", str(before_context)])
|
|
98
|
+
if context is not None and mode == OutputMode.CONTENT:
|
|
99
|
+
args.extend(["--context", str(context)])
|
|
100
|
+
if multiline:
|
|
101
|
+
args.append("--multiline")
|
|
102
|
+
if head_limit is not None:
|
|
103
|
+
args.extend(["--max-count", str(head_limit)])
|
|
104
|
+
|
|
105
|
+
# Pattern + path
|
|
106
|
+
args.append(pattern)
|
|
107
|
+
args.append(str(search_path))
|
|
108
|
+
|
|
109
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
110
|
+
return result.stdout
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# glob_search — file pattern matching
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def glob_search(
|
|
118
|
+
pattern: str,
|
|
119
|
+
*,
|
|
120
|
+
path: Optional[str] = None,
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""Find files by glob pattern.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
pattern:
|
|
127
|
+
Glob pattern (e.g. ``"**/*.py"``, ``"src/**/*.ts"``).
|
|
128
|
+
path:
|
|
129
|
+
Base directory. Defaults to cwd.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
List of matching absolute file paths.
|
|
134
|
+
"""
|
|
135
|
+
base = Path(path) if path else Path.cwd()
|
|
136
|
+
matches = sorted(base.glob(pattern))
|
|
137
|
+
return [str(m.resolve()) for m in matches if not any(p.startswith(".") for p in m.parts)]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File Operation Tools — the triad of ``view``, ``create``, and ``edit``.
|
|
3
|
+
|
|
4
|
+
Safety-first file I/O:
|
|
5
|
+
view — Read files, directories, or images (read-only)
|
|
6
|
+
create — Write a new file (write-once, refuses if exists)
|
|
7
|
+
edit — Surgical single-occurrence string replacement (write-many)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from copilot_tools.file_operations.tools import view, create, edit
|
|
11
|
+
|
|
12
|
+
__all__ = ["view", "create", "edit"]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem primitives: view, create, edit.
|
|
3
|
+
|
|
4
|
+
All paths must be absolute (enforced via ``pathlib.Path``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import mimetypes
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Constants
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
MAX_FILE_SIZE = 50 * 1024 # 50 KB
|
|
19
|
+
IMAGE_EXTENSIONS = {
|
|
20
|
+
".png", ".jpg", ".jpeg", ".gif", ".svg",
|
|
21
|
+
".webp", ".bmp", ".ico", ".tiff", ".tif",
|
|
22
|
+
}
|
|
23
|
+
DIR_LIST_DEPTH = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Internal helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _ensure_absolute(path: Path) -> None:
|
|
31
|
+
if not path.is_absolute():
|
|
32
|
+
raise ValueError(f"Path MUST be absolute, got: {path}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _add_line_numbers(content: str) -> str:
|
|
36
|
+
lines = content.splitlines(keepends=True)
|
|
37
|
+
numbered = []
|
|
38
|
+
for i, line in enumerate(lines, start=1):
|
|
39
|
+
numbered.append(f"{i}. {line.rstrip()}")
|
|
40
|
+
return "\n".join(numbered)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _list_directory(path: Path, *, depth: int = DIR_LIST_DEPTH, _current: int = 0) -> str:
|
|
44
|
+
"""Recursively list non-hidden entries up to *depth* levels."""
|
|
45
|
+
entries: list[str] = []
|
|
46
|
+
indent = " " * _current
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
children = sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
|
|
50
|
+
except PermissionError:
|
|
51
|
+
return f"{indent}{path.name}/ [permission denied]"
|
|
52
|
+
|
|
53
|
+
for child in children:
|
|
54
|
+
if child.name.startswith("."):
|
|
55
|
+
continue
|
|
56
|
+
if child.is_dir():
|
|
57
|
+
entries.append(f"{indent}{child.name}/")
|
|
58
|
+
if _current < depth - 1:
|
|
59
|
+
entries.append(_list_directory(child, depth=depth, _current=_current + 1))
|
|
60
|
+
else:
|
|
61
|
+
entries.append(f"{indent}{child.name}")
|
|
62
|
+
|
|
63
|
+
return "\n".join(entries)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_image(path: Path) -> bool:
|
|
67
|
+
return path.suffix.lower() in IMAGE_EXTENSIONS
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Public API
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def view(
|
|
75
|
+
path: str,
|
|
76
|
+
*,
|
|
77
|
+
view_range: Optional[tuple[int, int]] = None,
|
|
78
|
+
force_read_large_files: bool = False,
|
|
79
|
+
) -> str | dict:
|
|
80
|
+
"""View a file, directory, or image.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
path:
|
|
85
|
+
Absolute path to file or directory. Must exist.
|
|
86
|
+
view_range:
|
|
87
|
+
``(start, end)`` — 1-indexed line range. ``end=-1`` means EOF.
|
|
88
|
+
force_read_large_files:
|
|
89
|
+
Skip the 50 KB truncation guard.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
* **str** with line-numbered content for regular files.
|
|
94
|
+
* **dict** ``{"base64": ..., "mimeType": ...}`` for images.
|
|
95
|
+
* **str** directory listing for directories.
|
|
96
|
+
"""
|
|
97
|
+
p = Path(path)
|
|
98
|
+
_ensure_absolute(p)
|
|
99
|
+
|
|
100
|
+
if not p.exists():
|
|
101
|
+
raise FileNotFoundError(f"File MUST exist to view: {p}")
|
|
102
|
+
|
|
103
|
+
# --- Directory ---
|
|
104
|
+
if p.is_dir():
|
|
105
|
+
return _list_directory(p)
|
|
106
|
+
|
|
107
|
+
# --- Image ---
|
|
108
|
+
if _is_image(p):
|
|
109
|
+
data = p.read_bytes()
|
|
110
|
+
mime = mimetypes.guess_type(str(p))[0] or "application/octet-stream"
|
|
111
|
+
return {"base64": base64.b64encode(data).decode(), "mimeType": mime}
|
|
112
|
+
|
|
113
|
+
# --- Regular file ---
|
|
114
|
+
size = p.stat().st_size
|
|
115
|
+
if size > MAX_FILE_SIZE and not force_read_large_files:
|
|
116
|
+
content = p.read_text(errors="replace")[:MAX_FILE_SIZE]
|
|
117
|
+
return (
|
|
118
|
+
_add_line_numbers(content)
|
|
119
|
+
+ "\n[File truncated at 50KB. Use view_range to read specific sections.]"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
content = p.read_text(errors="replace")
|
|
123
|
+
|
|
124
|
+
if view_range is not None:
|
|
125
|
+
start, end = view_range
|
|
126
|
+
lines = content.splitlines()
|
|
127
|
+
if end == -1:
|
|
128
|
+
end = len(lines)
|
|
129
|
+
content = "\n".join(lines[start - 1 : end])
|
|
130
|
+
|
|
131
|
+
return _add_line_numbers(content)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def create(path: str, file_text: str) -> dict:
|
|
135
|
+
"""Create a new file. Refuses if the file already exists.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
path:
|
|
140
|
+
Absolute path. File MUST NOT exist.
|
|
141
|
+
file_text:
|
|
142
|
+
Complete content to write.
|
|
143
|
+
"""
|
|
144
|
+
p = Path(path)
|
|
145
|
+
_ensure_absolute(p)
|
|
146
|
+
|
|
147
|
+
if p.exists():
|
|
148
|
+
raise FileExistsError(
|
|
149
|
+
f"File already exists at {p}. Cannot overwrite. Use edit() for existing files."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not p.parent.exists():
|
|
153
|
+
raise FileNotFoundError(
|
|
154
|
+
f"Parent directory does not exist: {p.parent}. Create it first."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
p.write_text(file_text)
|
|
158
|
+
return {"success": True, "path": str(p), "bytes_written": len(file_text)}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def edit(path: str, old_str: str, new_str: str) -> dict:
|
|
162
|
+
"""Replace exactly one occurrence of *old_str* with *new_str*.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
path:
|
|
167
|
+
Absolute path. File MUST exist.
|
|
168
|
+
old_str:
|
|
169
|
+
Exact text to find (must appear exactly once).
|
|
170
|
+
new_str:
|
|
171
|
+
Replacement text.
|
|
172
|
+
"""
|
|
173
|
+
p = Path(path)
|
|
174
|
+
_ensure_absolute(p)
|
|
175
|
+
|
|
176
|
+
if not p.exists():
|
|
177
|
+
raise FileNotFoundError(
|
|
178
|
+
f"File MUST exist to edit: {p}. Use create() for new files."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
content = p.read_text(errors="replace")
|
|
182
|
+
occurrences = content.count(old_str)
|
|
183
|
+
|
|
184
|
+
if occurrences == 0:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"old_str not found in {p}. "
|
|
187
|
+
"Ensure the string matches exactly, including whitespace."
|
|
188
|
+
)
|
|
189
|
+
if occurrences > 1:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"old_str appears {occurrences} times in {p}. "
|
|
192
|
+
"Not unique — include more surrounding context."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
new_content = content.replace(old_str, new_str, 1)
|
|
196
|
+
p.write_text(new_content)
|
|
197
|
+
return {"success": True, "path": str(p)}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown Analysis Tools — structured extraction from markdown documents.
|
|
3
|
+
|
|
4
|
+
get_overview — Full eagle-eye overview (stats, structure, intro)
|
|
5
|
+
get_headers — Extract all headers with line numbers
|
|
6
|
+
get_section — Extract section content under a specific header
|
|
7
|
+
get_intro — Extract introduction/abstract/summary
|
|
8
|
+
get_links — Extract HTTP/HTTPS links
|
|
9
|
+
get_tables_metadata — Table metadata (headers, line numbers, column counts)
|
|
10
|
+
get_table — Extract and format a specific table by line number
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .tools import (
|
|
14
|
+
markdown_analyzer_get_headers as get_headers,
|
|
15
|
+
markdown_analyzer_get_header_by_line as get_section,
|
|
16
|
+
markdown_analyzer_get_intro as get_intro,
|
|
17
|
+
markdown_analyzer_get_overview as get_overview,
|
|
18
|
+
markdown_analyzer_get_links as get_links,
|
|
19
|
+
markdown_analyzer_get_table_by_line as get_table,
|
|
20
|
+
markdown_analyzer_get_tables_metadata as get_tables_metadata,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"get_overview",
|
|
25
|
+
"get_headers",
|
|
26
|
+
"get_section",
|
|
27
|
+
"get_intro",
|
|
28
|
+
"get_links",
|
|
29
|
+
"get_tables_metadata",
|
|
30
|
+
"get_table",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
tools = {
|
|
34
|
+
"markdown_get_overview": get_overview,
|
|
35
|
+
"markdown_get_headers": get_headers,
|
|
36
|
+
"markdown_get_section": get_section,
|
|
37
|
+
"markdown_get_intro": get_intro,
|
|
38
|
+
"markdown_get_links": get_links,
|
|
39
|
+
"markdown_get_tables_metadata": get_tables_metadata,
|
|
40
|
+
"markdown_get_table": get_table,
|
|
41
|
+
}
|