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,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
+ }