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.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|