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,296 @@
1
+ """
2
+ Agent orchestration: launch_task, read_agent, list_agents, execute_skill.
3
+
4
+ Implements a hierarchical manager-worker pattern where a main agent delegates
5
+ tasks to isolated sub-agents.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import time
12
+ import uuid
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import Any, Callable, Optional
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Enums & constants
20
+ # ---------------------------------------------------------------------------
21
+
22
+ class AgentType(str, Enum):
23
+ EXPLORE = "explore"
24
+ TASK = "task"
25
+ GENERAL_PURPOSE = "general-purpose"
26
+ CODE_REVIEW = "code-review"
27
+
28
+
29
+ class AgentStatus(str, Enum):
30
+ RUNNING = "running"
31
+ IDLE = "idle"
32
+ COMPLETED = "completed"
33
+ FAILED = "failed"
34
+ CANCELLED = "cancelled"
35
+
36
+
37
+ DEFAULT_MODELS: dict[AgentType, str] = {
38
+ AgentType.EXPLORE: "claude-haiku-4.5",
39
+ AgentType.TASK: "claude-haiku-4.5",
40
+ AgentType.GENERAL_PURPOSE: "claude-sonnet-4.6",
41
+ AgentType.CODE_REVIEW: "claude-sonnet-4.6",
42
+ }
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Agent data structures
47
+ # ---------------------------------------------------------------------------
48
+
49
+ @dataclass
50
+ class Turn:
51
+ turn_index: int
52
+ role: str
53
+ content: str
54
+
55
+
56
+ @dataclass
57
+ class Agent:
58
+ agent_id: str
59
+ name: str
60
+ agent_type: AgentType
61
+ description: str
62
+ model: str
63
+ prompt: str
64
+ status: AgentStatus = AgentStatus.RUNNING
65
+ turns: list[Turn] = field(default_factory=list)
66
+ _thread: Optional[threading.Thread] = field(default=None, repr=False)
67
+ _handler: Optional[Callable[..., str]] = field(default=None, repr=False)
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # AgentManager — singleton registry
72
+ # ---------------------------------------------------------------------------
73
+
74
+ class AgentManager:
75
+ def __init__(self) -> None:
76
+ self._agents: dict[str, Agent] = {}
77
+ self._lock = threading.Lock()
78
+
79
+ def register(self, agent: Agent) -> None:
80
+ with self._lock:
81
+ self._agents[agent.agent_id] = agent
82
+
83
+ def get(self, agent_id: str) -> Optional[Agent]:
84
+ return self._agents.get(agent_id)
85
+
86
+ def all_agents(self, *, include_completed: bool = True) -> list[Agent]:
87
+ agents = list(self._agents.values())
88
+ if not include_completed:
89
+ agents = [
90
+ a for a in agents
91
+ if a.status in (AgentStatus.RUNNING, AgentStatus.IDLE)
92
+ ]
93
+ return agents
94
+
95
+
96
+ _manager = AgentManager()
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Default handler (stub — replace with real LLM call in production)
101
+ # ---------------------------------------------------------------------------
102
+
103
+ def _default_handler(agent: Agent) -> None:
104
+ """Placeholder that simulates agent work. Override in production."""
105
+ agent.turns.append(
106
+ Turn(
107
+ turn_index=0,
108
+ role="assistant",
109
+ content=(
110
+ f"[stub] Agent '{agent.name}' ({agent.agent_type.value}) "
111
+ f"received prompt: {agent.prompt[:200]}..."
112
+ ),
113
+ )
114
+ )
115
+ agent.status = AgentStatus.COMPLETED
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Public API
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def launch_task(
123
+ name: str,
124
+ prompt: str,
125
+ agent_type: str,
126
+ description: str,
127
+ *,
128
+ model: Optional[str] = None,
129
+ mode: str = "background",
130
+ handler: Optional[Callable[..., None]] = None,
131
+ ) -> dict:
132
+ """Launch a specialised sub-agent.
133
+
134
+ Parameters
135
+ ----------
136
+ name:
137
+ Short human-readable name (e.g. ``"auth-flow"``).
138
+ prompt:
139
+ Complete task description with all context.
140
+ agent_type:
141
+ ``"explore"`` | ``"task"`` | ``"general-purpose"`` | ``"code-review"``.
142
+ description:
143
+ 3-5 word UI summary.
144
+ model:
145
+ Override the default model for the agent type.
146
+ mode:
147
+ ``"background"`` (default) or ``"sync"``.
148
+ handler:
149
+ Custom callable ``(Agent) -> None``. Defaults to a stub.
150
+
151
+ Returns
152
+ -------
153
+ dict with ``agent_id`` and ``status``.
154
+ """
155
+ atype = AgentType(agent_type)
156
+ agent_id = f"{name}-{uuid.uuid4().hex[:4]}"
157
+
158
+ agent = Agent(
159
+ agent_id=agent_id,
160
+ name=name,
161
+ agent_type=atype,
162
+ description=description,
163
+ model=model or DEFAULT_MODELS[atype],
164
+ prompt=prompt,
165
+ )
166
+ _manager.register(agent)
167
+
168
+ work_fn = handler or _default_handler
169
+
170
+ if mode == "sync":
171
+ try:
172
+ work_fn(agent)
173
+ except Exception as exc:
174
+ agent.status = AgentStatus.FAILED
175
+ agent.turns.append(
176
+ Turn(turn_index=len(agent.turns), role="error", content=str(exc))
177
+ )
178
+ return {"agent_id": agent_id, "status": agent.status.value}
179
+
180
+ # background
181
+ def _run() -> None:
182
+ try:
183
+ work_fn(agent)
184
+ except Exception as exc:
185
+ agent.status = AgentStatus.FAILED
186
+ agent.turns.append(
187
+ Turn(turn_index=len(agent.turns), role="error", content=str(exc))
188
+ )
189
+
190
+ t = threading.Thread(target=_run, daemon=True)
191
+ agent._thread = t
192
+ t.start()
193
+
194
+ return {"agent_id": agent_id, "status": "running"}
195
+
196
+
197
+ def read_agent(
198
+ agent_id: str,
199
+ *,
200
+ wait: bool = False,
201
+ timeout: float = 30,
202
+ since_turn: Optional[int] = None,
203
+ ) -> dict:
204
+ """Read status and results from a background agent.
205
+
206
+ Parameters
207
+ ----------
208
+ agent_id:
209
+ ID returned by :func:`launch_task`.
210
+ wait:
211
+ Block until the agent completes.
212
+ timeout:
213
+ Max seconds to wait (only with ``wait=True``). Max 180.
214
+ since_turn:
215
+ Only return turns after this index (exclusive).
216
+ """
217
+ agent = _manager.get(agent_id)
218
+ if agent is None:
219
+ raise KeyError(f"No agent with id={agent_id!r}")
220
+
221
+ timeout = min(timeout, 180)
222
+
223
+ if wait and agent._thread is not None:
224
+ agent._thread.join(timeout=timeout)
225
+
226
+ turns = agent.turns
227
+ if since_turn is not None:
228
+ turns = [t for t in turns if t.turn_index > since_turn]
229
+
230
+ return {
231
+ "agent_id": agent.agent_id,
232
+ "status": agent.status.value,
233
+ "turns": [
234
+ {"turn_index": t.turn_index, "role": t.role, "content": t.content}
235
+ for t in turns
236
+ ],
237
+ }
238
+
239
+
240
+ def list_agents(*, include_completed: bool = True) -> list[dict]:
241
+ """List all agents.
242
+
243
+ Parameters
244
+ ----------
245
+ include_completed:
246
+ When ``False``, only return ``running`` and ``idle`` agents.
247
+ """
248
+ return [
249
+ {
250
+ "agent_id": a.agent_id,
251
+ "name": a.name,
252
+ "agent_type": a.agent_type.value,
253
+ "status": a.status.value,
254
+ "description": a.description,
255
+ }
256
+ for a in _manager.all_agents(include_completed=include_completed)
257
+ ]
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Skills
262
+ # ---------------------------------------------------------------------------
263
+
264
+ # Skill registry — register custom skills with ``register_skill(name, fn)``
265
+ _skill_registry: dict[str, Callable[..., Any]] = {}
266
+
267
+
268
+ def register_skill(name: str, fn: Callable[..., Any]) -> None:
269
+ """Register a skill handler for use with :func:`execute_skill`."""
270
+ _skill_registry[name] = fn
271
+
272
+
273
+ def execute_skill(skill: str) -> Any:
274
+ """Execute a named skill in the main conversation context.
275
+
276
+ Parameters
277
+ ----------
278
+ skill:
279
+ Skill name (e.g. ``"pdf"``, ``"xlsx"``, ``"customize-cloud-agent"``).
280
+
281
+ Returns
282
+ -------
283
+ Whatever the skill handler returns.
284
+
285
+ Raises
286
+ ------
287
+ KeyError
288
+ If the skill is not registered.
289
+ """
290
+ fn = _skill_registry.get(skill)
291
+ if fn is None:
292
+ raise KeyError(
293
+ f"Skill {skill!r} is not registered. "
294
+ f"Available: {list(_skill_registry.keys())}"
295
+ )
296
+ return fn()
@@ -0,0 +1,20 @@
1
+ """
2
+ Bash Execution Tools — interactive shell session management.
3
+
4
+ Provides five tools that give a full interactive shell:
5
+ bash — Run a command (sync or async), create/reuse sessions
6
+ write_bash — Send input (text + key sequences) to a running session
7
+ read_bash — Read accumulated output from a session
8
+ stop_bash — Terminate a session and clean up resources
9
+ list_bash — List all active sessions
10
+ """
11
+
12
+ from copilot_tools.bash_execution.sessions import (
13
+ bash,
14
+ write_bash,
15
+ read_bash,
16
+ stop_bash,
17
+ list_bash,
18
+ )
19
+
20
+ __all__ = ["bash", "write_bash", "read_bash", "stop_bash", "list_bash"]
@@ -0,0 +1,335 @@
1
+ """
2
+ Session-oriented PTY shell management.
3
+
4
+ Each session owns a pseudo-terminal (PTY), a PID, and persistent env vars.
5
+ Sessions are identified by a string ``shellId`` and managed through a global
6
+ ``SessionManager``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import signal
13
+ import subprocess
14
+ import threading
15
+ import time
16
+ import uuid
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum
19
+ from typing import Optional
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Constants & key-map
24
+ # ---------------------------------------------------------------------------
25
+
26
+ class Mode(str, Enum):
27
+ SYNC = "sync"
28
+ ASYNC = "async"
29
+
30
+
31
+ KEY_MAP: dict[str, str] = {
32
+ "{up}": "\x1b[A",
33
+ "{down}": "\x1b[B",
34
+ "{right}": "\x1b[C",
35
+ "{left}": "\x1b[D",
36
+ "{enter}": "\n",
37
+ "{backspace}": "\x7f",
38
+ }
39
+
40
+ DEFAULT_INITIAL_WAIT = 30
41
+ MIN_INITIAL_WAIT = 30
42
+ MAX_INITIAL_WAIT = 600
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Session dataclass
47
+ # ---------------------------------------------------------------------------
48
+
49
+ @dataclass
50
+ class Session:
51
+ shell_id: str
52
+ command: str
53
+ mode: Mode
54
+ pid: Optional[int] = None
55
+ process: Optional[subprocess.Popen] = None
56
+ detach: bool = False
57
+ _output_buffer: str = ""
58
+ _lock: threading.Lock = field(default_factory=threading.Lock)
59
+
60
+ # -- output helpers -----------------------------------------------------
61
+
62
+ def append_output(self, text: str) -> None:
63
+ with self._lock:
64
+ self._output_buffer += text
65
+
66
+ def read_output(self) -> str:
67
+ with self._lock:
68
+ buf = self._output_buffer
69
+ self._output_buffer = ""
70
+ return buf
71
+
72
+ def full_output(self) -> str:
73
+ with self._lock:
74
+ return self._output_buffer
75
+
76
+ # -- status helpers -----------------------------------------------------
77
+
78
+ @property
79
+ def is_running(self) -> bool:
80
+ if self.process is None:
81
+ return False
82
+ return self.process.poll() is None
83
+
84
+ @property
85
+ def exit_code(self) -> Optional[int]:
86
+ if self.process is None:
87
+ return None
88
+ return self.process.poll()
89
+
90
+ @property
91
+ def has_unread_output(self) -> bool:
92
+ with self._lock:
93
+ return len(self._output_buffer) > 0
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # SessionManager — singleton pool of sessions
98
+ # ---------------------------------------------------------------------------
99
+
100
+ class SessionManager:
101
+ def __init__(self) -> None:
102
+ self._sessions: dict[str, Session] = {}
103
+ self._lock = threading.Lock()
104
+
105
+ def get_or_create(self, shell_id: Optional[str], mode: Mode) -> Session:
106
+ if shell_id and shell_id in self._sessions:
107
+ return self._sessions[shell_id]
108
+ sid = shell_id or f"session-{uuid.uuid4().hex[:8]}"
109
+ session = Session(shell_id=sid, command="", mode=mode)
110
+ with self._lock:
111
+ self._sessions[sid] = session
112
+ return session
113
+
114
+ def get(self, shell_id: str) -> Optional[Session]:
115
+ return self._sessions.get(shell_id)
116
+
117
+ def remove(self, shell_id: str) -> None:
118
+ with self._lock:
119
+ self._sessions.pop(shell_id, None)
120
+
121
+ def all_sessions(self) -> list[Session]:
122
+ return list(self._sessions.values())
123
+
124
+
125
+ _manager = SessionManager()
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Internal helpers
130
+ # ---------------------------------------------------------------------------
131
+
132
+ def _translate_keys(raw_input: str) -> str:
133
+ """Replace ``{enter}``, ``{up}`` etc. with ANSI escape sequences."""
134
+ result = raw_input
135
+ for token, escape in KEY_MAP.items():
136
+ result = result.replace(token, escape)
137
+ return result
138
+
139
+
140
+ def _collect_output(session: Session, timeout: float) -> str:
141
+ """Block until the process finishes *or* ``timeout`` expires, collecting stdout/stderr."""
142
+ if session.process is None:
143
+ return ""
144
+ try:
145
+ stdout, _ = session.process.communicate(timeout=timeout)
146
+ session.append_output(stdout)
147
+ except subprocess.TimeoutExpired:
148
+ pass
149
+ return session.full_output()
150
+
151
+
152
+ def _start_process(command: str, detach: bool) -> subprocess.Popen:
153
+ kwargs: dict = dict(
154
+ shell=True,
155
+ executable="/bin/bash",
156
+ stdout=subprocess.PIPE,
157
+ stderr=subprocess.STDOUT,
158
+ text=True,
159
+ )
160
+ if detach:
161
+ kwargs["start_new_session"] = True
162
+ return subprocess.Popen(command, **kwargs)
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Public API
167
+ # ---------------------------------------------------------------------------
168
+
169
+ def bash(
170
+ command: str,
171
+ description: str,
172
+ *,
173
+ mode: str = "sync",
174
+ initial_wait: int = DEFAULT_INITIAL_WAIT,
175
+ shell_id: Optional[str] = None,
176
+ detach: bool = False,
177
+ ) -> dict:
178
+ """Run a Bash command in an interactive session.
179
+
180
+ Parameters
181
+ ----------
182
+ command:
183
+ The shell command to run.
184
+ description:
185
+ Human-readable description (max 100 chars), displayed in the UI.
186
+ mode:
187
+ ``"sync"`` (default) — wait for output; ``"async"`` — run in background.
188
+ initial_wait:
189
+ Seconds (30-600) to wait for initial output in sync mode.
190
+ shell_id:
191
+ Reuse an existing session. Auto-generated when ``None``.
192
+ detach:
193
+ Async-only. If ``True`` the process survives agent shutdown.
194
+
195
+ Returns
196
+ -------
197
+ dict with keys: ``output``, ``exitCode``, ``shellId``, ``status``.
198
+ """
199
+ initial_wait = max(MIN_INITIAL_WAIT, min(initial_wait, MAX_INITIAL_WAIT))
200
+ run_mode = Mode(mode)
201
+
202
+ session = _manager.get_or_create(shell_id, run_mode)
203
+ session.command = command
204
+ session.mode = run_mode
205
+ session.detach = detach
206
+
207
+ proc = _start_process(command, detach=(run_mode == Mode.ASYNC and detach))
208
+ session.process = proc
209
+ session.pid = proc.pid
210
+
211
+ if run_mode == Mode.SYNC:
212
+ output = _collect_output(session, timeout=initial_wait)
213
+
214
+ if session.is_running:
215
+ return {
216
+ "output": output,
217
+ "exitCode": None,
218
+ "shellId": session.shell_id,
219
+ "status": "running",
220
+ }
221
+
222
+ return {
223
+ "output": session.full_output(),
224
+ "exitCode": session.exit_code,
225
+ "shellId": session.shell_id,
226
+ "status": "completed",
227
+ }
228
+
229
+ # async — return immediately
230
+ return {
231
+ "output": "",
232
+ "shellId": session.shell_id,
233
+ "status": "running",
234
+ }
235
+
236
+
237
+ def write_bash(
238
+ shell_id: str,
239
+ *,
240
+ input_text: Optional[str] = None,
241
+ delay: float = 10,
242
+ ) -> dict:
243
+ """Send input to a running session, then read output after *delay* seconds.
244
+
245
+ Parameters
246
+ ----------
247
+ shell_id:
248
+ Session to write to.
249
+ input_text:
250
+ Text / key tokens (``{enter}``, ``{up}``, …) to send.
251
+ delay:
252
+ Seconds to wait before reading the output buffer.
253
+ """
254
+ session = _manager.get(shell_id)
255
+ if session is None:
256
+ raise KeyError(f"No session with shellId={shell_id!r}")
257
+
258
+ if input_text and session.process and session.process.stdin:
259
+ translated = _translate_keys(input_text)
260
+ session.process.stdin.write(translated)
261
+ session.process.stdin.flush()
262
+
263
+ time.sleep(delay)
264
+ return {"output": session.read_output()}
265
+
266
+
267
+ def read_bash(shell_id: str, *, delay: float = 5) -> dict:
268
+ """Read accumulated output from a running session.
269
+
270
+ Parameters
271
+ ----------
272
+ shell_id:
273
+ Session to read from.
274
+ delay:
275
+ Seconds to wait before reading.
276
+ """
277
+ session = _manager.get(shell_id)
278
+ if session is None:
279
+ raise KeyError(f"No session with shellId={shell_id!r}")
280
+
281
+ time.sleep(delay)
282
+
283
+ if session.process and session.is_running:
284
+ try:
285
+ stdout, _ = session.process.communicate(timeout=0.5)
286
+ session.append_output(stdout)
287
+ except subprocess.TimeoutExpired:
288
+ pass
289
+
290
+ return {"output": session.read_output()}
291
+
292
+
293
+ def stop_bash(shell_id: str) -> dict:
294
+ """Terminate a Bash session and clean up resources.
295
+
296
+ Sends SIGTERM → grace period → SIGKILL if needed.
297
+ """
298
+ session = _manager.get(shell_id)
299
+ if session is None:
300
+ raise KeyError(f"No session with shellId={shell_id!r}")
301
+
302
+ if session.process and session.is_running:
303
+ pid = session.process.pid
304
+ try:
305
+ pgid = os.getpgid(pid)
306
+ os.killpg(pgid, signal.SIGTERM)
307
+ except (ProcessLookupError, PermissionError):
308
+ pass
309
+
310
+ try:
311
+ session.process.wait(timeout=5)
312
+ except subprocess.TimeoutExpired:
313
+ try:
314
+ pgid = os.getpgid(pid)
315
+ os.killpg(pgid, signal.SIGKILL)
316
+ except (ProcessLookupError, PermissionError):
317
+ pass
318
+
319
+ _manager.remove(shell_id)
320
+ return {"status": "terminated", "shellId": shell_id}
321
+
322
+
323
+ def list_bash() -> list[dict]:
324
+ """Return info about all active sessions."""
325
+ return [
326
+ {
327
+ "shellId": s.shell_id,
328
+ "command": s.command,
329
+ "mode": s.mode.value,
330
+ "pid": s.pid,
331
+ "status": "running" if s.is_running else "completed",
332
+ "unreadOutput": s.has_unread_output,
333
+ }
334
+ for s in _manager.all_sessions()
335
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ Code Search Tools — fast content and filename searching.
3
+
4
+ grep — Search file *contents* via regex (wraps ripgrep)
5
+ glob_search — Search file *names* via glob patterns
6
+ """
7
+
8
+ from copilot_tools.code_search.tools import grep, glob_search
9
+
10
+ __all__ = ["grep", "glob_search"]