voidx 1.0.0__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 (126) hide show
  1. voidx/__init__.py +3 -0
  2. voidx/agent/__init__.py +0 -0
  3. voidx/agent/agents.py +439 -0
  4. voidx/agent/attachments.py +235 -0
  5. voidx/agent/graph.py +463 -0
  6. voidx/agent/graph_components/__init__.py +1 -0
  7. voidx/agent/graph_components/compaction.py +268 -0
  8. voidx/agent/graph_components/permissions.py +139 -0
  9. voidx/agent/graph_components/run_loop.py +532 -0
  10. voidx/agent/graph_components/runtime.py +14 -0
  11. voidx/agent/graph_components/streaming.py +351 -0
  12. voidx/agent/graph_components/subagent.py +278 -0
  13. voidx/agent/graph_components/tool_execution.py +208 -0
  14. voidx/agent/runtime_context.py +368 -0
  15. voidx/agent/slash.py +466 -0
  16. voidx/agent/slash_components/__init__.py +1 -0
  17. voidx/agent/slash_components/code_ide.py +68 -0
  18. voidx/agent/slash_components/lsp.py +105 -0
  19. voidx/agent/slash_components/mcp.py +332 -0
  20. voidx/agent/slash_components/model.py +419 -0
  21. voidx/agent/slash_components/runtime.py +55 -0
  22. voidx/agent/slash_components/skills.py +94 -0
  23. voidx/agent/state.py +32 -0
  24. voidx/agent/task_state.py +278 -0
  25. voidx/agent/tool_filters.py +27 -0
  26. voidx/config.py +707 -0
  27. voidx/llm/__init__.py +0 -0
  28. voidx/llm/catalog.py +188 -0
  29. voidx/llm/compaction.py +267 -0
  30. voidx/llm/context.py +43 -0
  31. voidx/llm/instruction.py +220 -0
  32. voidx/llm/provider.py +312 -0
  33. voidx/llm/usage.py +341 -0
  34. voidx/lsp/__init__.py +30 -0
  35. voidx/lsp/client.py +259 -0
  36. voidx/lsp/config.py +172 -0
  37. voidx/lsp/detector.py +512 -0
  38. voidx/lsp/errors.py +19 -0
  39. voidx/lsp/manager.py +280 -0
  40. voidx/lsp/schema.py +179 -0
  41. voidx/lsp/service.py +103 -0
  42. voidx/main.py +154 -0
  43. voidx/mcp/__init__.py +33 -0
  44. voidx/mcp/client.py +458 -0
  45. voidx/mcp/manager.py +267 -0
  46. voidx/mcp/schema.py +112 -0
  47. voidx/mcp/tool.py +122 -0
  48. voidx/mcp_servers/__init__.py +1 -0
  49. voidx/mcp_servers/web.py +104 -0
  50. voidx/memory/__init__.py +0 -0
  51. voidx/memory/context_frames.py +188 -0
  52. voidx/memory/model_profiles.py +98 -0
  53. voidx/memory/runtime_state.py +240 -0
  54. voidx/memory/session.py +272 -0
  55. voidx/memory/store.py +245 -0
  56. voidx/memory/transcript.py +137 -0
  57. voidx/permission/__init__.py +28 -0
  58. voidx/permission/engine.py +430 -0
  59. voidx/permission/evaluate.py +114 -0
  60. voidx/permission/sandbox.py +280 -0
  61. voidx/permission/schema.py +24 -0
  62. voidx/permission/service.py +314 -0
  63. voidx/permission/wildcard.py +34 -0
  64. voidx/skills/__init__.py +18 -0
  65. voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
  66. voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
  67. voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
  68. voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
  69. voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
  70. voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
  71. voidx/skills/policy.py +97 -0
  72. voidx/skills/registry.py +162 -0
  73. voidx/skills/schema.py +47 -0
  74. voidx/skills/service.py +199 -0
  75. voidx/tools/__init__.py +0 -0
  76. voidx/tools/agent.py +81 -0
  77. voidx/tools/base.py +86 -0
  78. voidx/tools/bash.py +105 -0
  79. voidx/tools/file_ops.py +193 -0
  80. voidx/tools/lsp.py +155 -0
  81. voidx/tools/registry.py +104 -0
  82. voidx/tools/repomap.py +238 -0
  83. voidx/tools/search.py +162 -0
  84. voidx/tools/task_status.py +57 -0
  85. voidx/tools/task_tracker.py +81 -0
  86. voidx/tools/todo.py +82 -0
  87. voidx/tools/web_content.py +357 -0
  88. voidx/tools/web_mcp.py +107 -0
  89. voidx/tools/webfetch.py +155 -0
  90. voidx/tools/websearch.py +276 -0
  91. voidx/ui/__init__.py +0 -0
  92. voidx/ui/app.py +1033 -0
  93. voidx/ui/app_components/__init__.py +1 -0
  94. voidx/ui/app_components/clipboard_image.py +245 -0
  95. voidx/ui/app_components/commands.py +18 -0
  96. voidx/ui/app_components/controls.py +29 -0
  97. voidx/ui/app_components/file_picker.py +115 -0
  98. voidx/ui/app_components/formatting.py +187 -0
  99. voidx/ui/app_components/git_changes.py +51 -0
  100. voidx/ui/app_components/rendering.py +1169 -0
  101. voidx/ui/browse.py +160 -0
  102. voidx/ui/capture.py +169 -0
  103. voidx/ui/code_ide.py +251 -0
  104. voidx/ui/commands.py +83 -0
  105. voidx/ui/console.py +381 -0
  106. voidx/ui/console_components/__init__.py +1 -0
  107. voidx/ui/console_components/formatting.py +96 -0
  108. voidx/ui/console_components/streaming.py +253 -0
  109. voidx/ui/diff.py +331 -0
  110. voidx/ui/dock.py +372 -0
  111. voidx/ui/dock_components/__init__.py +1 -0
  112. voidx/ui/dock_components/formatting.py +123 -0
  113. voidx/ui/dock_components/nodes.py +401 -0
  114. voidx/ui/dock_components/state.py +51 -0
  115. voidx/ui/event_components/__init__.py +1 -0
  116. voidx/ui/event_components/schema.py +249 -0
  117. voidx/ui/events.py +341 -0
  118. voidx/ui/session_changes.py +163 -0
  119. voidx/ui/startup.py +161 -0
  120. voidx/ui/transcript.py +148 -0
  121. voidx/ui/tree.py +316 -0
  122. voidx-1.0.0.dist-info/METADATA +59 -0
  123. voidx-1.0.0.dist-info/RECORD +126 -0
  124. voidx-1.0.0.dist-info/WHEEL +5 -0
  125. voidx-1.0.0.dist-info/entry_points.txt +2 -0
  126. voidx-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,332 @@
1
+ """Slash command support for /mcp operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shlex
7
+ import sys
8
+ from collections.abc import Mapping
9
+
10
+ from voidx.agent.slash_components.runtime import _select_from_list, ui
11
+ from voidx.config import McpServerConfig, WebToolRoute
12
+
13
+
14
+ class SlashMcpMixin:
15
+ async def _mcp(self, args: str) -> None:
16
+ parts = args.split(None, 1)
17
+ action = parts[0] if parts else ""
18
+ target = parts[1].strip() if len(parts) > 1 else ""
19
+
20
+ if action == "new":
21
+ await self._mcp_new()
22
+ elif action == "list":
23
+ await self._mcp_list()
24
+ elif action == "test" or action.startswith("test "):
25
+ await self._mcp_test(target)
26
+ elif action == "del" or action.startswith("del "):
27
+ await self._mcp_del(target)
28
+ elif action == "restart" or action.startswith("restart "):
29
+ await self._mcp_restart(target)
30
+ elif action == "tools" or action.startswith("tools "):
31
+ await self._mcp_tools(target)
32
+ elif action:
33
+ ui.error("Usage: /mcp [new|list|test|del|restart|tools]")
34
+ else:
35
+ await self._mcp_list()
36
+
37
+ async def _mcp_new(self) -> None:
38
+ settings = self._g._settings
39
+ if settings is None:
40
+ ui.error("No settings file available.")
41
+ return
42
+
43
+ ui.print("[bold]Configure MCP server[/bold]")
44
+ choices = ["voidx-web (built-in)", "Tavily MCP", "Custom command"]
45
+ idx = await _select_from_list(self._g._app, "MCP server type", choices)
46
+ if idx is None:
47
+ ui.print("[dim]Cancelled.[/dim]")
48
+ return
49
+
50
+ web_routes: Mapping[str, WebToolRoute] = {}
51
+ if choices[idx].startswith("voidx-web"):
52
+ server = await self._mcp_builtin_web_config()
53
+ if server is None:
54
+ return
55
+ web_routes = {
56
+ "search": WebToolRoute(backend="mcp", server=server.name, tool="web_search"),
57
+ "fetch": WebToolRoute(backend="mcp", server=server.name, tool="web_fetch"),
58
+ }
59
+ elif choices[idx].startswith("Tavily"):
60
+ server = await self._mcp_tavily_config()
61
+ if server is None:
62
+ return
63
+ web_routes = {
64
+ "search": WebToolRoute(backend="mcp", server=server.name, tool="tavily_search"),
65
+ "fetch": WebToolRoute(backend="mcp", server=server.name, tool="tavily_extract"),
66
+ }
67
+ else:
68
+ server = await self._mcp_custom_config()
69
+ if server is None:
70
+ return
71
+
72
+ ok, tools, err = await self._test_mcp_config(server)
73
+ if not ok:
74
+ ui.error(f"MCP connection failed: {err}")
75
+ ui.print("[dim]Configuration not saved. Check the command and try again.[/dim]")
76
+ return
77
+
78
+ tool_names = [tool.name for tool in tools]
79
+ if tool_names:
80
+ server.tools = tool_names
81
+
82
+ path = settings.save_mcp_server(server)
83
+ for kind, route in web_routes.items():
84
+ settings.set_web_tool_route(kind, route)
85
+
86
+ manager = getattr(self._g, "_mcp_manager", None)
87
+ if manager is not None:
88
+ await manager.restart_all()
89
+
90
+ ui.print(
91
+ f" [cyan]{server.name}[/cyan] [green]✓ configured[/green]"
92
+ f" · {len(tool_names)} tool{'s' if len(tool_names) != 1 else ''}"
93
+ )
94
+ if web_routes:
95
+ ui.print("[dim]websearch/webfetch now use this MCP server[/dim]")
96
+ ui.print(f"[dim]Saved to {path}[/dim]")
97
+
98
+ async def _mcp_builtin_web_config(self) -> McpServerConfig | None:
99
+ name = await self._prompt("Server name", default="voidx-web")
100
+ if name is None:
101
+ ui.print("[dim]Cancelled.[/dim]")
102
+ return None
103
+ name = name.strip() or "voidx-web"
104
+ env = {}
105
+ tavily_key = self._g._settings.get_tavily_api_key() if self._g._settings else None
106
+ if tavily_key:
107
+ env["TAVILY_API_KEY"] = tavily_key
108
+ return McpServerConfig(
109
+ name=name,
110
+ command=sys.executable,
111
+ args=["-m", "voidx.mcp_servers.web"],
112
+ env=env,
113
+ tools=["web_search", "web_fetch"],
114
+ )
115
+
116
+ async def _mcp_tavily_config(self) -> McpServerConfig | None:
117
+ name = await self._prompt("Server name", default="tavily")
118
+ if name is None:
119
+ ui.print("[dim]Cancelled.[/dim]")
120
+ return None
121
+ name = name.strip() or "tavily"
122
+
123
+ env = {}
124
+ env_key = os.environ.get("TAVILY_API_KEY")
125
+ tavily_key = self._g._settings.get_tavily_api_key() if self._g._settings else None
126
+ if not env_key and tavily_key:
127
+ env["TAVILY_API_KEY"] = tavily_key
128
+ elif not env_key:
129
+ tavily_key = await self._prompt("Tavily API key", secret=True)
130
+ if tavily_key is None:
131
+ ui.print("[dim]Cancelled.[/dim]")
132
+ return None
133
+ tavily_key = tavily_key.strip()
134
+ if not tavily_key:
135
+ ui.error("Tavily API key is required.")
136
+ return None
137
+ env["TAVILY_API_KEY"] = tavily_key
138
+
139
+ return McpServerConfig(
140
+ name=name,
141
+ command="npx",
142
+ args=["-y", "tavily-mcp@latest"],
143
+ env=env,
144
+ tools=["tavily_search", "tavily_extract"],
145
+ )
146
+
147
+ async def _mcp_custom_config(self) -> McpServerConfig | None:
148
+ name = await self._prompt("Server name")
149
+ if name is None:
150
+ ui.print("[dim]Cancelled.[/dim]")
151
+ return None
152
+ name = name.strip()
153
+ if not name:
154
+ ui.error("Server name is required.")
155
+ return None
156
+
157
+ command = await self._prompt("Command")
158
+ if command is None:
159
+ ui.print("[dim]Cancelled.[/dim]")
160
+ return None
161
+ command = command.strip()
162
+ if not command:
163
+ ui.error("Command is required.")
164
+ return None
165
+
166
+ args_text = await self._prompt("Args (shell-style, optional)", default="")
167
+ if args_text is None:
168
+ ui.print("[dim]Cancelled.[/dim]")
169
+ return None
170
+ try:
171
+ args = shlex.split(args_text)
172
+ except ValueError as exc:
173
+ ui.error(f"Invalid args: {exc}")
174
+ return None
175
+
176
+ env_text = await self._prompt("Env VAR=value,VAR2=value2 (optional)", default="")
177
+ if env_text is None:
178
+ ui.print("[dim]Cancelled.[/dim]")
179
+ return None
180
+ env = _parse_env_pairs(env_text)
181
+ return McpServerConfig(name=name, command=command, args=args, env=env)
182
+
183
+ async def _mcp_list(self) -> None:
184
+ settings = self._g._settings
185
+ if settings is None:
186
+ ui.print("[dim]No settings file available.[/dim]")
187
+ return
188
+
189
+ manager = getattr(self._g, "_mcp_manager", None)
190
+ statuses = manager.statuses() if manager is not None and manager.started else []
191
+ ui.print("[bold]MCP servers:[/bold]")
192
+ ui.print(f"[dim]{settings.path}[/dim]")
193
+ if statuses:
194
+ for status in statuses:
195
+ self._print_mcp_status(status)
196
+ else:
197
+ servers = settings.list_mcp_servers()
198
+ if not servers:
199
+ ui.print("[dim]No MCP servers configured. Use /mcp new.[/dim]")
200
+ return
201
+ for server in servers:
202
+ state = "[dim]disabled[/dim]" if server.disabled else "[green]configured[/green]"
203
+ tools = f"{server.tool_count} tool{'s' if server.tool_count != 1 else ''}"
204
+ ui.print(f" [cyan]{server.name}[/cyan] · {state} · [dim]{tools}[/dim]")
205
+
206
+ search = settings.get_web_tool_route("search")
207
+ fetch = settings.get_web_tool_route("fetch")
208
+ if search.backend == "mcp" or fetch.backend == "mcp":
209
+ ui.print()
210
+ ui.print("[bold]Web routing:[/bold]")
211
+ ui.print(f" search · {search.backend} {search.server}/{search.tool}".rstrip("/"))
212
+ ui.print(f" fetch · {fetch.backend} {fetch.server}/{fetch.tool}".rstrip("/"))
213
+ ui.print("[dim]Usage: /mcp new|list|test|del|restart|tools[/dim]")
214
+
215
+ async def _mcp_test(self, target: str) -> None:
216
+ async def _do_test(name: str) -> None:
217
+ server = self._g._settings.get_mcp_server(name) if self._g._settings else None
218
+ if server is None:
219
+ ui.error(f"MCP server not found: {name}")
220
+ return
221
+ ok, tools, err = await self._test_mcp_config(server)
222
+ if ok:
223
+ names = ", ".join(tool.name for tool in tools) or "no tools"
224
+ ui.print(f"[green]✓ {name} — connected[/green] [dim]{names}[/dim]")
225
+ else:
226
+ ui.print(f"[red]✗ {name} — {err}[/red]")
227
+
228
+ await self._pick_mcp_server("Test", target, _do_test)
229
+
230
+ async def _mcp_del(self, target: str) -> None:
231
+ async def _do_delete(name: str) -> None:
232
+ if self._g._settings is None:
233
+ ui.error("No settings file available.")
234
+ return
235
+ path = self._g._settings.delete_mcp_server(name)
236
+ manager = getattr(self._g, "_mcp_manager", None)
237
+ if manager is not None:
238
+ await manager.restart_all()
239
+ ui.print(f"[dim]'{name}' removed.[/dim]")
240
+ ui.print(f"[dim]Cleaned {path}[/dim]")
241
+
242
+ await self._pick_mcp_server("Delete", target, _do_delete)
243
+
244
+ async def _mcp_restart(self, target: str) -> None:
245
+ _ = target
246
+ manager = getattr(self._g, "_mcp_manager", None)
247
+ if manager is None:
248
+ ui.error("No MCP manager available.")
249
+ return
250
+ await manager.restart_all()
251
+ ui.print("[green]✓ MCP servers restarted[/green]")
252
+
253
+ async def _mcp_tools(self, target: str) -> None:
254
+ async def _do_tools(name: str) -> None:
255
+ manager = getattr(self._g, "_mcp_manager", None)
256
+ if manager is None:
257
+ ui.error("No MCP manager available.")
258
+ return
259
+ try:
260
+ tools = await manager.list_tools_for_server(name)
261
+ except Exception as exc:
262
+ ui.error(f"Could not list tools for {name}: {exc}")
263
+ return
264
+ ui.print(f"[bold]{name} tools:[/bold]")
265
+ if not tools:
266
+ ui.print("[dim]No tools.[/dim]")
267
+ return
268
+ for tool in tools:
269
+ ui.print(f" [cyan]{tool.name}[/cyan] — {tool.description or '(no description)'}")
270
+
271
+ await self._pick_mcp_server("Tools", target, _do_tools)
272
+
273
+ async def _pick_mcp_server(self, action: str, target: str, callback) -> None:
274
+ if self._g._settings is None:
275
+ ui.error("No settings file available.")
276
+ return
277
+ if target:
278
+ await callback(target)
279
+ return
280
+ names = [server.name for server in self._g._settings.list_mcp_servers()]
281
+ if not names:
282
+ ui.print("[yellow]No MCP servers configured. Use /mcp new first.[/yellow]")
283
+ return
284
+ ui.print(f"[bold]{action}[/bold] — select MCP server:")
285
+ idx = await _select_from_list(self._g._app, action, names)
286
+ if idx is None:
287
+ ui.print("[dim]Cancelled.[/dim]")
288
+ return
289
+ await callback(names[idx])
290
+
291
+ @staticmethod
292
+ async def _test_mcp_config(server: McpServerConfig):
293
+ from voidx.mcp.client import McpClient
294
+
295
+ client = McpClient(server)
296
+ try:
297
+ await client.start()
298
+ tools = await client.list_tools()
299
+ return True, tools, ""
300
+ except Exception as exc:
301
+ return False, [], str(exc)
302
+ finally:
303
+ await client.stop()
304
+
305
+ @staticmethod
306
+ def _print_mcp_status(status) -> None:
307
+ if status.status == "connected":
308
+ state = "[green]connected[/green]"
309
+ elif status.status == "error":
310
+ state = "[red]error[/red]"
311
+ elif status.status == "disabled":
312
+ state = "[dim]disabled[/dim]"
313
+ else:
314
+ state = "[yellow]disconnected[/yellow]"
315
+ tools = f"{status.tool_count} tool{'s' if status.tool_count != 1 else ''}" if status.tool_count else ""
316
+ err = f" · [dim]{status.error_message}[/dim]" if status.error_message else ""
317
+ ui.print(f" [cyan]{status.name}[/cyan] · {state}{f' · {tools}' if tools else ''}{err}")
318
+
319
+
320
+ def _parse_env_pairs(text: str) -> dict[str, str]:
321
+ result: dict[str, str] = {}
322
+ for raw in text.split(","):
323
+ item = raw.strip()
324
+ if not item:
325
+ continue
326
+ if "=" not in item:
327
+ continue
328
+ key, value = item.split("=", 1)
329
+ key = key.strip()
330
+ if key:
331
+ result[key] = value.strip()
332
+ return result