nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker Agent for NC1709
|
|
3
|
+
Handles Docker and Docker Compose operations
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from ..base import (
|
|
13
|
+
Plugin, PluginMetadata, PluginCapability,
|
|
14
|
+
ActionResult
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ContainerInfo:
|
|
20
|
+
"""Represents a Docker container"""
|
|
21
|
+
id: str
|
|
22
|
+
name: str
|
|
23
|
+
image: str
|
|
24
|
+
status: str
|
|
25
|
+
ports: str
|
|
26
|
+
created: str
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_running(self) -> bool:
|
|
30
|
+
return "Up" in self.status
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ImageInfo:
|
|
35
|
+
"""Represents a Docker image"""
|
|
36
|
+
id: str
|
|
37
|
+
repository: str
|
|
38
|
+
tag: str
|
|
39
|
+
size: str
|
|
40
|
+
created: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DockerAgent(Plugin):
|
|
44
|
+
"""
|
|
45
|
+
Docker operations agent.
|
|
46
|
+
|
|
47
|
+
Provides Docker and Docker Compose operations:
|
|
48
|
+
- Container management (list, start, stop, remove)
|
|
49
|
+
- Image management (list, pull, build, remove)
|
|
50
|
+
- Docker Compose operations
|
|
51
|
+
- Log viewing and inspection
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
METADATA = PluginMetadata(
|
|
55
|
+
name="docker",
|
|
56
|
+
version="1.0.0",
|
|
57
|
+
description="Docker container management",
|
|
58
|
+
author="NC1709 Team",
|
|
59
|
+
capabilities=[
|
|
60
|
+
PluginCapability.CONTAINER_MANAGEMENT,
|
|
61
|
+
PluginCapability.COMMAND_EXECUTION
|
|
62
|
+
],
|
|
63
|
+
keywords=[
|
|
64
|
+
"docker", "container", "image", "compose", "build",
|
|
65
|
+
"pull", "push", "run", "stop", "start", "logs",
|
|
66
|
+
"dockerfile", "docker-compose", "volume", "network"
|
|
67
|
+
],
|
|
68
|
+
config_schema={
|
|
69
|
+
"compose_file": {"type": "string", "default": "docker-compose.yml"},
|
|
70
|
+
"default_registry": {"type": "string", "default": ""},
|
|
71
|
+
"build_context": {"type": "string", "default": "."}
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def metadata(self) -> PluginMetadata:
|
|
77
|
+
return self.METADATA
|
|
78
|
+
|
|
79
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
80
|
+
super().__init__(config)
|
|
81
|
+
self._docker_available = False
|
|
82
|
+
self._compose_available = False
|
|
83
|
+
|
|
84
|
+
def initialize(self) -> bool:
|
|
85
|
+
"""Initialize the Docker agent"""
|
|
86
|
+
# Check Docker
|
|
87
|
+
try:
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
["docker", "--version"],
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True
|
|
92
|
+
)
|
|
93
|
+
self._docker_available = result.returncode == 0
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
self._error = "Docker is not installed"
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# Check Docker Compose
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["docker", "compose", "version"],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True
|
|
104
|
+
)
|
|
105
|
+
self._compose_available = result.returncode == 0
|
|
106
|
+
except Exception:
|
|
107
|
+
# Try legacy docker-compose
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
["docker-compose", "--version"],
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True
|
|
113
|
+
)
|
|
114
|
+
self._compose_available = result.returncode == 0
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
return self._docker_available
|
|
119
|
+
|
|
120
|
+
def cleanup(self) -> None:
|
|
121
|
+
"""Cleanup resources"""
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def _register_actions(self) -> None:
|
|
125
|
+
"""Register Docker actions"""
|
|
126
|
+
# Container actions
|
|
127
|
+
self.register_action(
|
|
128
|
+
"ps",
|
|
129
|
+
self.list_containers,
|
|
130
|
+
"List containers",
|
|
131
|
+
parameters={"all": {"type": "boolean", "default": False}}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.register_action(
|
|
135
|
+
"start",
|
|
136
|
+
self.start_container,
|
|
137
|
+
"Start a container",
|
|
138
|
+
parameters={"container": {"type": "string", "required": True}}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self.register_action(
|
|
142
|
+
"stop",
|
|
143
|
+
self.stop_container,
|
|
144
|
+
"Stop a container",
|
|
145
|
+
parameters={"container": {"type": "string", "required": True}},
|
|
146
|
+
requires_confirmation=True
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self.register_action(
|
|
150
|
+
"remove",
|
|
151
|
+
self.remove_container,
|
|
152
|
+
"Remove a container",
|
|
153
|
+
parameters={
|
|
154
|
+
"container": {"type": "string", "required": True},
|
|
155
|
+
"force": {"type": "boolean", "default": False}
|
|
156
|
+
},
|
|
157
|
+
requires_confirmation=True,
|
|
158
|
+
dangerous=True
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self.register_action(
|
|
162
|
+
"logs",
|
|
163
|
+
self.get_logs,
|
|
164
|
+
"View container logs",
|
|
165
|
+
parameters={
|
|
166
|
+
"container": {"type": "string", "required": True},
|
|
167
|
+
"tail": {"type": "integer", "default": 100},
|
|
168
|
+
"follow": {"type": "boolean", "default": False}
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self.register_action(
|
|
173
|
+
"exec",
|
|
174
|
+
self.exec_in_container,
|
|
175
|
+
"Execute command in container",
|
|
176
|
+
parameters={
|
|
177
|
+
"container": {"type": "string", "required": True},
|
|
178
|
+
"command": {"type": "string", "required": True}
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Image actions
|
|
183
|
+
self.register_action(
|
|
184
|
+
"images",
|
|
185
|
+
self.list_images,
|
|
186
|
+
"List images"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self.register_action(
|
|
190
|
+
"pull",
|
|
191
|
+
self.pull_image,
|
|
192
|
+
"Pull an image",
|
|
193
|
+
parameters={"image": {"type": "string", "required": True}}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.register_action(
|
|
197
|
+
"build",
|
|
198
|
+
self.build_image,
|
|
199
|
+
"Build an image",
|
|
200
|
+
parameters={
|
|
201
|
+
"tag": {"type": "string", "required": True},
|
|
202
|
+
"dockerfile": {"type": "string", "default": "Dockerfile"},
|
|
203
|
+
"context": {"type": "string", "default": "."}
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
self.register_action(
|
|
208
|
+
"rmi",
|
|
209
|
+
self.remove_image,
|
|
210
|
+
"Remove an image",
|
|
211
|
+
parameters={
|
|
212
|
+
"image": {"type": "string", "required": True},
|
|
213
|
+
"force": {"type": "boolean", "default": False}
|
|
214
|
+
},
|
|
215
|
+
requires_confirmation=True,
|
|
216
|
+
dangerous=True
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Compose actions
|
|
220
|
+
self.register_action(
|
|
221
|
+
"compose_up",
|
|
222
|
+
self.compose_up,
|
|
223
|
+
"Start services with docker-compose",
|
|
224
|
+
parameters={
|
|
225
|
+
"detach": {"type": "boolean", "default": True},
|
|
226
|
+
"build": {"type": "boolean", "default": False},
|
|
227
|
+
"services": {"type": "array", "optional": True}
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self.register_action(
|
|
232
|
+
"compose_down",
|
|
233
|
+
self.compose_down,
|
|
234
|
+
"Stop services with docker-compose",
|
|
235
|
+
parameters={
|
|
236
|
+
"volumes": {"type": "boolean", "default": False},
|
|
237
|
+
"remove_orphans": {"type": "boolean", "default": False}
|
|
238
|
+
},
|
|
239
|
+
requires_confirmation=True
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
self.register_action(
|
|
243
|
+
"compose_ps",
|
|
244
|
+
self.compose_ps,
|
|
245
|
+
"List compose services"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Utility actions
|
|
249
|
+
self.register_action(
|
|
250
|
+
"prune",
|
|
251
|
+
self.prune,
|
|
252
|
+
"Remove unused resources",
|
|
253
|
+
parameters={
|
|
254
|
+
"type": {"type": "string", "enum": ["containers", "images", "volumes", "all"]}
|
|
255
|
+
},
|
|
256
|
+
requires_confirmation=True,
|
|
257
|
+
dangerous=True
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def _run_docker(self, *args, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
261
|
+
"""Run a docker command"""
|
|
262
|
+
cmd = ["docker"] + list(args)
|
|
263
|
+
return subprocess.run(
|
|
264
|
+
cmd,
|
|
265
|
+
capture_output=True,
|
|
266
|
+
text=True,
|
|
267
|
+
timeout=timeout
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _run_compose(self, *args, timeout: int = 120) -> subprocess.CompletedProcess:
|
|
271
|
+
"""Run a docker-compose command"""
|
|
272
|
+
compose_file = self._config.get("compose_file", "docker-compose.yml")
|
|
273
|
+
|
|
274
|
+
# Try new syntax first
|
|
275
|
+
cmd = ["docker", "compose", "-f", compose_file] + list(args)
|
|
276
|
+
try:
|
|
277
|
+
result = subprocess.run(
|
|
278
|
+
cmd,
|
|
279
|
+
capture_output=True,
|
|
280
|
+
text=True,
|
|
281
|
+
timeout=timeout
|
|
282
|
+
)
|
|
283
|
+
if result.returncode == 0 or "unknown docker command" not in result.stderr.lower():
|
|
284
|
+
return result
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
# Fall back to docker-compose
|
|
289
|
+
cmd = ["docker-compose", "-f", compose_file] + list(args)
|
|
290
|
+
return subprocess.run(
|
|
291
|
+
cmd,
|
|
292
|
+
capture_output=True,
|
|
293
|
+
text=True,
|
|
294
|
+
timeout=timeout
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Container operations
|
|
298
|
+
|
|
299
|
+
def list_containers(self, all: bool = False) -> ActionResult:
|
|
300
|
+
"""List Docker containers
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
all: Include stopped containers
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
ActionResult with container list
|
|
307
|
+
"""
|
|
308
|
+
args = ["ps", "--format", "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}"]
|
|
309
|
+
|
|
310
|
+
if all:
|
|
311
|
+
args.append("-a")
|
|
312
|
+
|
|
313
|
+
result = self._run_docker(*args)
|
|
314
|
+
|
|
315
|
+
if result.returncode != 0:
|
|
316
|
+
return ActionResult.fail(result.stderr)
|
|
317
|
+
|
|
318
|
+
containers = []
|
|
319
|
+
for line in result.stdout.strip().split("\n"):
|
|
320
|
+
if not line:
|
|
321
|
+
continue
|
|
322
|
+
parts = line.split("|")
|
|
323
|
+
if len(parts) >= 6:
|
|
324
|
+
containers.append(ContainerInfo(
|
|
325
|
+
id=parts[0],
|
|
326
|
+
name=parts[1],
|
|
327
|
+
image=parts[2],
|
|
328
|
+
status=parts[3],
|
|
329
|
+
ports=parts[4],
|
|
330
|
+
created=parts[5]
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
running = sum(1 for c in containers if c.is_running)
|
|
334
|
+
|
|
335
|
+
return ActionResult.ok(
|
|
336
|
+
message=f"{len(containers)} containers ({running} running)",
|
|
337
|
+
data=containers
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def start_container(self, container: str) -> ActionResult:
|
|
341
|
+
"""Start a container"""
|
|
342
|
+
result = self._run_docker("start", container)
|
|
343
|
+
|
|
344
|
+
if result.returncode != 0:
|
|
345
|
+
return ActionResult.fail(result.stderr)
|
|
346
|
+
|
|
347
|
+
return ActionResult.ok(f"Started container: {container}")
|
|
348
|
+
|
|
349
|
+
def stop_container(self, container: str) -> ActionResult:
|
|
350
|
+
"""Stop a container"""
|
|
351
|
+
result = self._run_docker("stop", container)
|
|
352
|
+
|
|
353
|
+
if result.returncode != 0:
|
|
354
|
+
return ActionResult.fail(result.stderr)
|
|
355
|
+
|
|
356
|
+
return ActionResult.ok(f"Stopped container: {container}")
|
|
357
|
+
|
|
358
|
+
def remove_container(self, container: str, force: bool = False) -> ActionResult:
|
|
359
|
+
"""Remove a container"""
|
|
360
|
+
args = ["rm"]
|
|
361
|
+
if force:
|
|
362
|
+
args.append("-f")
|
|
363
|
+
args.append(container)
|
|
364
|
+
|
|
365
|
+
result = self._run_docker(*args)
|
|
366
|
+
|
|
367
|
+
if result.returncode != 0:
|
|
368
|
+
return ActionResult.fail(result.stderr)
|
|
369
|
+
|
|
370
|
+
return ActionResult.ok(f"Removed container: {container}")
|
|
371
|
+
|
|
372
|
+
def get_logs(
|
|
373
|
+
self,
|
|
374
|
+
container: str,
|
|
375
|
+
tail: int = 100,
|
|
376
|
+
follow: bool = False
|
|
377
|
+
) -> ActionResult:
|
|
378
|
+
"""Get container logs"""
|
|
379
|
+
args = ["logs", f"--tail={tail}"]
|
|
380
|
+
|
|
381
|
+
if follow:
|
|
382
|
+
# For follow, we'd need streaming - just get latest
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
args.append(container)
|
|
386
|
+
|
|
387
|
+
result = self._run_docker(*args)
|
|
388
|
+
|
|
389
|
+
if result.returncode != 0:
|
|
390
|
+
return ActionResult.fail(result.stderr)
|
|
391
|
+
|
|
392
|
+
# Combine stdout and stderr (logs can go to either)
|
|
393
|
+
logs = result.stdout + result.stderr
|
|
394
|
+
|
|
395
|
+
return ActionResult.ok(
|
|
396
|
+
message=f"Logs for {container} (last {tail} lines)",
|
|
397
|
+
data=logs
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def exec_in_container(self, container: str, command: str) -> ActionResult:
|
|
401
|
+
"""Execute command in container"""
|
|
402
|
+
args = ["exec", container] + command.split()
|
|
403
|
+
|
|
404
|
+
result = self._run_docker(*args)
|
|
405
|
+
|
|
406
|
+
return ActionResult.ok(
|
|
407
|
+
message=f"Executed in {container}",
|
|
408
|
+
data={"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Image operations
|
|
412
|
+
|
|
413
|
+
def list_images(self) -> ActionResult:
|
|
414
|
+
"""List Docker images"""
|
|
415
|
+
result = self._run_docker(
|
|
416
|
+
"images",
|
|
417
|
+
"--format", "{{.ID}}|{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if result.returncode != 0:
|
|
421
|
+
return ActionResult.fail(result.stderr)
|
|
422
|
+
|
|
423
|
+
images = []
|
|
424
|
+
for line in result.stdout.strip().split("\n"):
|
|
425
|
+
if not line:
|
|
426
|
+
continue
|
|
427
|
+
parts = line.split("|")
|
|
428
|
+
if len(parts) >= 5:
|
|
429
|
+
images.append(ImageInfo(
|
|
430
|
+
id=parts[0],
|
|
431
|
+
repository=parts[1],
|
|
432
|
+
tag=parts[2],
|
|
433
|
+
size=parts[3],
|
|
434
|
+
created=parts[4]
|
|
435
|
+
))
|
|
436
|
+
|
|
437
|
+
return ActionResult.ok(
|
|
438
|
+
message=f"{len(images)} images",
|
|
439
|
+
data=images
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def pull_image(self, image: str) -> ActionResult:
|
|
443
|
+
"""Pull an image"""
|
|
444
|
+
result = self._run_docker("pull", image, timeout=300)
|
|
445
|
+
|
|
446
|
+
if result.returncode != 0:
|
|
447
|
+
return ActionResult.fail(result.stderr)
|
|
448
|
+
|
|
449
|
+
return ActionResult.ok(
|
|
450
|
+
message=f"Pulled image: {image}",
|
|
451
|
+
data=result.stdout
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def build_image(
|
|
455
|
+
self,
|
|
456
|
+
tag: str,
|
|
457
|
+
dockerfile: str = "Dockerfile",
|
|
458
|
+
context: str = "."
|
|
459
|
+
) -> ActionResult:
|
|
460
|
+
"""Build a Docker image"""
|
|
461
|
+
args = ["build", "-t", tag, "-f", dockerfile, context]
|
|
462
|
+
|
|
463
|
+
result = self._run_docker(*args, timeout=600)
|
|
464
|
+
|
|
465
|
+
if result.returncode != 0:
|
|
466
|
+
return ActionResult.fail(result.stderr)
|
|
467
|
+
|
|
468
|
+
return ActionResult.ok(
|
|
469
|
+
message=f"Built image: {tag}",
|
|
470
|
+
data=result.stdout
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def remove_image(self, image: str, force: bool = False) -> ActionResult:
|
|
474
|
+
"""Remove an image"""
|
|
475
|
+
args = ["rmi"]
|
|
476
|
+
if force:
|
|
477
|
+
args.append("-f")
|
|
478
|
+
args.append(image)
|
|
479
|
+
|
|
480
|
+
result = self._run_docker(*args)
|
|
481
|
+
|
|
482
|
+
if result.returncode != 0:
|
|
483
|
+
return ActionResult.fail(result.stderr)
|
|
484
|
+
|
|
485
|
+
return ActionResult.ok(f"Removed image: {image}")
|
|
486
|
+
|
|
487
|
+
# Docker Compose operations
|
|
488
|
+
|
|
489
|
+
def compose_up(
|
|
490
|
+
self,
|
|
491
|
+
detach: bool = True,
|
|
492
|
+
build: bool = False,
|
|
493
|
+
services: Optional[List[str]] = None
|
|
494
|
+
) -> ActionResult:
|
|
495
|
+
"""Start docker-compose services"""
|
|
496
|
+
if not self._compose_available:
|
|
497
|
+
return ActionResult.fail("Docker Compose not available")
|
|
498
|
+
|
|
499
|
+
args = ["up"]
|
|
500
|
+
|
|
501
|
+
if detach:
|
|
502
|
+
args.append("-d")
|
|
503
|
+
if build:
|
|
504
|
+
args.append("--build")
|
|
505
|
+
|
|
506
|
+
if services:
|
|
507
|
+
args.extend(services)
|
|
508
|
+
|
|
509
|
+
result = self._run_compose(*args, timeout=300)
|
|
510
|
+
|
|
511
|
+
if result.returncode != 0:
|
|
512
|
+
return ActionResult.fail(result.stderr)
|
|
513
|
+
|
|
514
|
+
return ActionResult.ok(
|
|
515
|
+
message="Services started",
|
|
516
|
+
data=result.stdout
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
def compose_down(
|
|
520
|
+
self,
|
|
521
|
+
volumes: bool = False,
|
|
522
|
+
remove_orphans: bool = False
|
|
523
|
+
) -> ActionResult:
|
|
524
|
+
"""Stop docker-compose services"""
|
|
525
|
+
if not self._compose_available:
|
|
526
|
+
return ActionResult.fail("Docker Compose not available")
|
|
527
|
+
|
|
528
|
+
args = ["down"]
|
|
529
|
+
|
|
530
|
+
if volumes:
|
|
531
|
+
args.append("-v")
|
|
532
|
+
if remove_orphans:
|
|
533
|
+
args.append("--remove-orphans")
|
|
534
|
+
|
|
535
|
+
result = self._run_compose(*args)
|
|
536
|
+
|
|
537
|
+
if result.returncode != 0:
|
|
538
|
+
return ActionResult.fail(result.stderr)
|
|
539
|
+
|
|
540
|
+
return ActionResult.ok(
|
|
541
|
+
message="Services stopped",
|
|
542
|
+
data=result.stdout
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def compose_ps(self) -> ActionResult:
|
|
546
|
+
"""List docker-compose services"""
|
|
547
|
+
if not self._compose_available:
|
|
548
|
+
return ActionResult.fail("Docker Compose not available")
|
|
549
|
+
|
|
550
|
+
result = self._run_compose("ps")
|
|
551
|
+
|
|
552
|
+
if result.returncode != 0:
|
|
553
|
+
return ActionResult.fail(result.stderr)
|
|
554
|
+
|
|
555
|
+
return ActionResult.ok(
|
|
556
|
+
message="Compose services",
|
|
557
|
+
data=result.stdout
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Utility operations
|
|
561
|
+
|
|
562
|
+
def prune(self, type: str = "containers") -> ActionResult:
|
|
563
|
+
"""Remove unused Docker resources"""
|
|
564
|
+
if type == "containers":
|
|
565
|
+
result = self._run_docker("container", "prune", "-f")
|
|
566
|
+
elif type == "images":
|
|
567
|
+
result = self._run_docker("image", "prune", "-f")
|
|
568
|
+
elif type == "volumes":
|
|
569
|
+
result = self._run_docker("volume", "prune", "-f")
|
|
570
|
+
elif type == "all":
|
|
571
|
+
result = self._run_docker("system", "prune", "-f")
|
|
572
|
+
else:
|
|
573
|
+
return ActionResult.fail(f"Unknown prune type: {type}")
|
|
574
|
+
|
|
575
|
+
if result.returncode != 0:
|
|
576
|
+
return ActionResult.fail(result.stderr)
|
|
577
|
+
|
|
578
|
+
return ActionResult.ok(
|
|
579
|
+
message=f"Pruned {type}",
|
|
580
|
+
data=result.stdout
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def can_handle(self, request: str) -> float:
|
|
584
|
+
"""Check if request is Docker-related"""
|
|
585
|
+
request_lower = request.lower()
|
|
586
|
+
|
|
587
|
+
# High confidence
|
|
588
|
+
high_conf = ["docker", "container", "compose", "dockerfile"]
|
|
589
|
+
for kw in high_conf:
|
|
590
|
+
if kw in request_lower:
|
|
591
|
+
return 0.9
|
|
592
|
+
|
|
593
|
+
# Medium confidence
|
|
594
|
+
med_conf = ["image", "build", "deploy", "service"]
|
|
595
|
+
for kw in med_conf:
|
|
596
|
+
if kw in request_lower:
|
|
597
|
+
return 0.5
|
|
598
|
+
|
|
599
|
+
return super().can_handle(request)
|
|
600
|
+
|
|
601
|
+
def handle_request(self, request: str, **kwargs) -> Optional[ActionResult]:
|
|
602
|
+
"""Handle a natural language request"""
|
|
603
|
+
request_lower = request.lower()
|
|
604
|
+
|
|
605
|
+
# Container list
|
|
606
|
+
if any(kw in request_lower for kw in ["list containers", "ps", "running containers"]):
|
|
607
|
+
all_containers = "all" in request_lower or "stopped" in request_lower
|
|
608
|
+
return self.list_containers(all=all_containers)
|
|
609
|
+
|
|
610
|
+
# Images
|
|
611
|
+
if "list images" in request_lower or "show images" in request_lower:
|
|
612
|
+
return self.list_images()
|
|
613
|
+
|
|
614
|
+
# Compose
|
|
615
|
+
if "compose" in request_lower:
|
|
616
|
+
if "up" in request_lower or "start" in request_lower:
|
|
617
|
+
return self.compose_up()
|
|
618
|
+
if "down" in request_lower or "stop" in request_lower:
|
|
619
|
+
return self.compose_down()
|
|
620
|
+
if "ps" in request_lower or "status" in request_lower:
|
|
621
|
+
return self.compose_ps()
|
|
622
|
+
|
|
623
|
+
return None
|