pcli-agent 0.1.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.
- pcli/__init__.py +1 -0
- pcli/__main__.py +4 -0
- pcli/agent/__init__.py +0 -0
- pcli/agent/activity.py +116 -0
- pcli/agent/compaction.py +205 -0
- pcli/agent/context_pruning.py +88 -0
- pcli/agent/headless.py +209 -0
- pcli/agent/loop.py +442 -0
- pcli/agent/prompt.py +371 -0
- pcli/agent/runtime.py +240 -0
- pcli/browser/__init__.py +0 -0
- pcli/browser/session.py +135 -0
- pcli/cli.py +757 -0
- pcli/config/__init__.py +0 -0
- pcli/config/paths.py +95 -0
- pcli/config/settings.py +435 -0
- pcli/cost/__init__.py +0 -0
- pcli/cost/context.py +275 -0
- pcli/cost/context_detect.py +183 -0
- pcli/cost/pricing_table.py +141 -0
- pcli/cost/tracker.py +126 -0
- pcli/llm/__init__.py +0 -0
- pcli/llm/client.py +285 -0
- pcli/llm/errors.py +37 -0
- pcli/llm/models.py +100 -0
- pcli/llm/streaming.py +108 -0
- pcli/memory/__init__.py +0 -0
- pcli/memory/extraction.py +106 -0
- pcli/memory/models.py +103 -0
- pcli/memory/store.py +88 -0
- pcli/permissions/__init__.py +0 -0
- pcli/permissions/guardrails.py +219 -0
- pcli/permissions/manager.py +215 -0
- pcli/permissions/policy.py +70 -0
- pcli/sandbox/__init__.py +0 -0
- pcli/sandbox/base.py +50 -0
- pcli/sandbox/docker_backend.py +107 -0
- pcli/sandbox/limits.py +63 -0
- pcli/sandbox/null_backend.py +92 -0
- pcli/sandbox/selector.py +75 -0
- pcli/sandbox/subprocess_backend.py +376 -0
- pcli/scheduler/__init__.py +0 -0
- pcli/scheduler/daemon.py +194 -0
- pcli/scheduler/models.py +97 -0
- pcli/scheduler/runner.py +84 -0
- pcli/scheduler/store.py +75 -0
- pcli/scheduler/triggers.py +84 -0
- pcli/session/__init__.py +0 -0
- pcli/session/audit.py +122 -0
- pcli/session/directory_check.py +28 -0
- pcli/session/export.py +57 -0
- pcli/session/importer.py +92 -0
- pcli/session/models.py +168 -0
- pcli/session/store.py +127 -0
- pcli/telegram/__init__.py +0 -0
- pcli/telegram/bot.py +266 -0
- pcli/telegram/daemon.py +1197 -0
- pcli/telegram/permissions.py +131 -0
- pcli/telegram/sender.py +58 -0
- pcli/tools/__init__.py +0 -0
- pcli/tools/_nested_agent.py +204 -0
- pcli/tools/agent_tools.py +264 -0
- pcli/tools/agent_tools_store.py +69 -0
- pcli/tools/artifacts.py +47 -0
- pcli/tools/base.py +185 -0
- pcli/tools/builtin/__init__.py +0 -0
- pcli/tools/builtin/agent_tool_register_tool.py +100 -0
- pcli/tools/builtin/artifact_tool.py +212 -0
- pcli/tools/builtin/ask_tool.py +77 -0
- pcli/tools/builtin/browser_tool.py +253 -0
- pcli/tools/builtin/decision_tool.py +73 -0
- pcli/tools/builtin/describe_tool.py +389 -0
- pcli/tools/builtin/diff_tools.py +225 -0
- pcli/tools/builtin/fs_tools.py +371 -0
- pcli/tools/builtin/grep_tool.py +88 -0
- pcli/tools/builtin/memory_tool.py +108 -0
- pcli/tools/builtin/network_tools.py +107 -0
- pcli/tools/builtin/pip_tool.py +106 -0
- pcli/tools/builtin/shell_tool.py +240 -0
- pcli/tools/builtin/subagent_tool.py +146 -0
- pcli/tools/builtin/todo_tool.py +122 -0
- pcli/tools/builtin/toolbox_register_tool.py +76 -0
- pcli/tools/builtin/web_tools.py +322 -0
- pcli/tools/pydiscovery/__init__.py +0 -0
- pcli/tools/pydiscovery/cache.py +51 -0
- pcli/tools/pydiscovery/index.py +48 -0
- pcli/tools/pydiscovery/invoke.py +181 -0
- pcli/tools/pydiscovery/search.py +117 -0
- pcli/tools/registry.py +138 -0
- pcli/tools/toolbox/__init__.py +0 -0
- pcli/tools/toolbox/introspect.py +48 -0
- pcli/tools/toolbox/manager.py +336 -0
- pcli/tools/toolbox/plugin_base.py +51 -0
- pcli/tools/toolbox/plugins/__init__.py +6 -0
- pcli/tools/toolbox/plugins/httpd.py +99 -0
- pcli/tools/toolbox/plugins/kafka.py +162 -0
- pcli/tools/toolbox/plugins/kubectl.py +211 -0
- pcli/tools/toolbox/plugins/sge.py +146 -0
- pcli/tools/toolbox/store.py +65 -0
- pcli/tools/toolbox/synthesize.py +100 -0
- pcli/tui/__init__.py +0 -0
- pcli/tui/app.py +37 -0
- pcli/tui/screens/__init__.py +0 -0
- pcli/tui/screens/ask_question_modal.py +54 -0
- pcli/tui/screens/chat.py +2070 -0
- pcli/tui/screens/confirm_modal.py +39 -0
- pcli/tui/screens/models.py +43 -0
- pcli/tui/screens/permission_modal.py +71 -0
- pcli/tui/screens/sessions.py +162 -0
- pcli/tui/screens/subagent_activity_modal.py +71 -0
- pcli/tui/shell_passthrough.py +56 -0
- pcli/tui/styles/pcli.tcss +241 -0
- pcli/tui/themes.py +84 -0
- pcli/tui/widgets/__init__.py +0 -0
- pcli/tui/widgets/chat_input.py +240 -0
- pcli/tui/widgets/command_suggestions.py +33 -0
- pcli/tui/widgets/message_view.py +328 -0
- pcli/tui/widgets/paste_input.py +99 -0
- pcli/tui/widgets/paste_marker.py +69 -0
- pcli/tui/widgets/status_bar.py +133 -0
- pcli/tui/widgets/status_pane.py +58 -0
- pcli/util/__init__.py +0 -0
- pcli/util/ids.py +15 -0
- pcli/util/logging.py +18 -0
- pcli/util/text.py +10 -0
- pcli_agent-0.1.0.dist-info/METADATA +259 -0
- pcli_agent-0.1.0.dist-info/RECORD +130 -0
- pcli_agent-0.1.0.dist-info/WHEEL +4 -0
- pcli_agent-0.1.0.dist-info/entry_points.txt +2 -0
- pcli_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from pcli.tools.toolbox.plugins.httpd import HttpdPlugin
|
|
2
|
+
from pcli.tools.toolbox.plugins.kafka import KafkaPlugin
|
|
3
|
+
from pcli.tools.toolbox.plugins.kubectl import KubectlPlugin
|
|
4
|
+
from pcli.tools.toolbox.plugins.sge import SgePlugin
|
|
5
|
+
|
|
6
|
+
ALL_PLUGINS = [KubectlPlugin(), SgePlugin(), KafkaPlugin(), HttpdPlugin()]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Curated plugin for Apache httpd, via apachectl/apache2ctl."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pcli.tools.toolbox.plugin_base import CommandSpec, DetectionResult, ToolboxPlugin
|
|
8
|
+
|
|
9
|
+
_VERSION_RE = re.compile(r"Apache/(\d+)\.(\d+)\.(\d+)")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _configtest_args(_args: dict) -> list[str]:
|
|
13
|
+
return ["-t"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _dump_vhosts_args(_args: dict) -> list[str]:
|
|
17
|
+
return ["-S"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _dump_modules_args(_args: dict) -> list[str]:
|
|
21
|
+
return ["-M"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _graceful_args(_args: dict) -> list[str]:
|
|
25
|
+
return ["-k", "graceful"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _restart_args(_args: dict) -> list[str]:
|
|
29
|
+
return ["-k", "restart"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _stop_args(_args: dict) -> list[str]:
|
|
33
|
+
return ["-k", "stop"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HttpdPlugin(ToolboxPlugin):
|
|
37
|
+
software_name = "httpd"
|
|
38
|
+
binary_names = ("apachectl", "apache2ctl", "httpd", "apache2")
|
|
39
|
+
version_args = ("-v",)
|
|
40
|
+
|
|
41
|
+
def parse_version(self, version_output: str) -> tuple[int, ...] | None:
|
|
42
|
+
match = _VERSION_RE.search(version_output)
|
|
43
|
+
if not match:
|
|
44
|
+
return None
|
|
45
|
+
return tuple(int(g) for g in match.groups())
|
|
46
|
+
|
|
47
|
+
def build_tools(self, detection: DetectionResult) -> list[CommandSpec]:
|
|
48
|
+
binary = detection.binary_path
|
|
49
|
+
return [
|
|
50
|
+
CommandSpec(
|
|
51
|
+
name="configtest",
|
|
52
|
+
description="Check the Apache httpd configuration for syntax errors.",
|
|
53
|
+
parameters={"type": "object", "properties": {}},
|
|
54
|
+
build_args=_configtest_args,
|
|
55
|
+
binary_path=binary,
|
|
56
|
+
risk="read",
|
|
57
|
+
),
|
|
58
|
+
CommandSpec(
|
|
59
|
+
name="dump_vhosts",
|
|
60
|
+
description="Show the parsed virtual host configuration.",
|
|
61
|
+
parameters={"type": "object", "properties": {}},
|
|
62
|
+
build_args=_dump_vhosts_args,
|
|
63
|
+
binary_path=binary,
|
|
64
|
+
risk="read",
|
|
65
|
+
),
|
|
66
|
+
CommandSpec(
|
|
67
|
+
name="dump_modules",
|
|
68
|
+
description="List loaded Apache modules.",
|
|
69
|
+
parameters={"type": "object", "properties": {}},
|
|
70
|
+
build_args=_dump_modules_args,
|
|
71
|
+
binary_path=binary,
|
|
72
|
+
risk="read",
|
|
73
|
+
),
|
|
74
|
+
CommandSpec(
|
|
75
|
+
name="graceful_restart",
|
|
76
|
+
description="Gracefully restart httpd (reload config without dropping "
|
|
77
|
+
"existing connections).",
|
|
78
|
+
parameters={"type": "object", "properties": {}},
|
|
79
|
+
build_args=_graceful_args,
|
|
80
|
+
binary_path=binary,
|
|
81
|
+
risk="mutate",
|
|
82
|
+
),
|
|
83
|
+
CommandSpec(
|
|
84
|
+
name="restart",
|
|
85
|
+
description="Restart httpd.",
|
|
86
|
+
parameters={"type": "object", "properties": {}},
|
|
87
|
+
build_args=_restart_args,
|
|
88
|
+
binary_path=binary,
|
|
89
|
+
risk="mutate",
|
|
90
|
+
),
|
|
91
|
+
CommandSpec(
|
|
92
|
+
name="stop",
|
|
93
|
+
description="Stop httpd.",
|
|
94
|
+
parameters={"type": "object", "properties": {}},
|
|
95
|
+
build_args=_stop_args,
|
|
96
|
+
binary_path=binary,
|
|
97
|
+
risk="destructive",
|
|
98
|
+
),
|
|
99
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Curated plugin for Apache Kafka's admin CLIs (kafka-topics,
|
|
2
|
+
kafka-consumer-groups). Like SGE, this is a suite of separate scripts, so
|
|
3
|
+
sibling binaries are resolved independently in build_tools()."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
from pcli.tools.toolbox.plugin_base import CommandSpec, DetectionResult, ToolboxPlugin
|
|
11
|
+
|
|
12
|
+
_VERSION_RE = re.compile(r"(\d+)\.(\d+)\.(\d+)")
|
|
13
|
+
|
|
14
|
+
_BOOTSTRAP_PARAM = {
|
|
15
|
+
"bootstrap_server": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Kafka bootstrap server, e.g. 'localhost:9092'.",
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _which_any(*names: str) -> str | None:
|
|
23
|
+
for name in names:
|
|
24
|
+
if path := shutil.which(name):
|
|
25
|
+
return path
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _list_topics_args(args: dict) -> list[str]:
|
|
30
|
+
return ["--bootstrap-server", args["bootstrap_server"], "--list"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _describe_topic_args(args: dict) -> list[str]:
|
|
34
|
+
return ["--bootstrap-server", args["bootstrap_server"], "--describe", "--topic", args["topic"]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _create_topic_args(args: dict) -> list[str]:
|
|
38
|
+
return [
|
|
39
|
+
"--bootstrap-server",
|
|
40
|
+
args["bootstrap_server"],
|
|
41
|
+
"--create",
|
|
42
|
+
"--topic",
|
|
43
|
+
args["topic"],
|
|
44
|
+
"--partitions",
|
|
45
|
+
str(args.get("partitions", 1)),
|
|
46
|
+
"--replication-factor",
|
|
47
|
+
str(args.get("replication_factor", 1)),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _delete_topic_args(args: dict) -> list[str]:
|
|
52
|
+
return ["--bootstrap-server", args["bootstrap_server"], "--delete", "--topic", args["topic"]]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _list_consumer_groups_args(args: dict) -> list[str]:
|
|
56
|
+
return ["--bootstrap-server", args["bootstrap_server"], "--list"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _describe_consumer_group_args(args: dict) -> list[str]:
|
|
60
|
+
return ["--bootstrap-server", args["bootstrap_server"], "--describe", "--group", args["group"]]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class KafkaPlugin(ToolboxPlugin):
|
|
64
|
+
software_name = "kafka"
|
|
65
|
+
binary_names = ("kafka-topics.sh", "kafka-topics", "kafka-topics.bat")
|
|
66
|
+
version_args = ("--version",)
|
|
67
|
+
|
|
68
|
+
def parse_version(self, version_output: str) -> tuple[int, ...] | None:
|
|
69
|
+
match = _VERSION_RE.search(version_output)
|
|
70
|
+
if not match:
|
|
71
|
+
return None
|
|
72
|
+
return tuple(int(g) for g in match.groups())
|
|
73
|
+
|
|
74
|
+
def build_tools(self, detection: DetectionResult) -> list[CommandSpec]:
|
|
75
|
+
topics_bin = detection.binary_path
|
|
76
|
+
specs = [
|
|
77
|
+
CommandSpec(
|
|
78
|
+
name="list_topics",
|
|
79
|
+
description="List all Kafka topics.",
|
|
80
|
+
parameters={
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": _BOOTSTRAP_PARAM,
|
|
83
|
+
"required": ["bootstrap_server"],
|
|
84
|
+
},
|
|
85
|
+
build_args=_list_topics_args,
|
|
86
|
+
binary_path=topics_bin,
|
|
87
|
+
risk="read",
|
|
88
|
+
),
|
|
89
|
+
CommandSpec(
|
|
90
|
+
name="describe_topic",
|
|
91
|
+
description="Show partition/replica detail for a specific topic.",
|
|
92
|
+
parameters={
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {**_BOOTSTRAP_PARAM, "topic": {"type": "string"}},
|
|
95
|
+
"required": ["bootstrap_server", "topic"],
|
|
96
|
+
},
|
|
97
|
+
build_args=_describe_topic_args,
|
|
98
|
+
binary_path=topics_bin,
|
|
99
|
+
risk="read",
|
|
100
|
+
),
|
|
101
|
+
CommandSpec(
|
|
102
|
+
name="create_topic",
|
|
103
|
+
description="Create a new Kafka topic.",
|
|
104
|
+
parameters={
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
**_BOOTSTRAP_PARAM,
|
|
108
|
+
"topic": {"type": "string"},
|
|
109
|
+
"partitions": {"type": "integer"},
|
|
110
|
+
"replication_factor": {"type": "integer"},
|
|
111
|
+
},
|
|
112
|
+
"required": ["bootstrap_server", "topic"],
|
|
113
|
+
},
|
|
114
|
+
build_args=_create_topic_args,
|
|
115
|
+
binary_path=topics_bin,
|
|
116
|
+
risk="mutate",
|
|
117
|
+
),
|
|
118
|
+
CommandSpec(
|
|
119
|
+
name="delete_topic",
|
|
120
|
+
description="Delete a Kafka topic.",
|
|
121
|
+
parameters={
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {**_BOOTSTRAP_PARAM, "topic": {"type": "string"}},
|
|
124
|
+
"required": ["bootstrap_server", "topic"],
|
|
125
|
+
},
|
|
126
|
+
build_args=_delete_topic_args,
|
|
127
|
+
binary_path=topics_bin,
|
|
128
|
+
risk="destructive",
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
if consumer_groups_bin := _which_any(
|
|
133
|
+
"kafka-consumer-groups.sh", "kafka-consumer-groups", "kafka-consumer-groups.bat"
|
|
134
|
+
):
|
|
135
|
+
specs += [
|
|
136
|
+
CommandSpec(
|
|
137
|
+
name="list_consumer_groups",
|
|
138
|
+
description="List Kafka consumer groups.",
|
|
139
|
+
parameters={
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": _BOOTSTRAP_PARAM,
|
|
142
|
+
"required": ["bootstrap_server"],
|
|
143
|
+
},
|
|
144
|
+
build_args=_list_consumer_groups_args,
|
|
145
|
+
binary_path=consumer_groups_bin,
|
|
146
|
+
risk="read",
|
|
147
|
+
),
|
|
148
|
+
CommandSpec(
|
|
149
|
+
name="describe_consumer_group",
|
|
150
|
+
description="Show lag/offset detail for a specific consumer group.",
|
|
151
|
+
parameters={
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {**_BOOTSTRAP_PARAM, "group": {"type": "string"}},
|
|
154
|
+
"required": ["bootstrap_server", "group"],
|
|
155
|
+
},
|
|
156
|
+
build_args=_describe_consumer_group_args,
|
|
157
|
+
binary_path=consumer_groups_bin,
|
|
158
|
+
risk="read",
|
|
159
|
+
),
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
return specs
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Curated plugin for kubectl (Kubernetes CLI). One binary, many subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pcli.tools.toolbox.plugin_base import CommandSpec, DetectionResult, ToolboxPlugin
|
|
8
|
+
|
|
9
|
+
_VERSION_RE = re.compile(r"[Vv]ersion:\s*v?(\d+)\.(\d+)\.(\d+)")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_args(args: dict) -> list[str]:
|
|
13
|
+
argv = ["get", args["resource"]]
|
|
14
|
+
if args.get("name"):
|
|
15
|
+
argv.append(args["name"])
|
|
16
|
+
if args.get("all_namespaces"):
|
|
17
|
+
argv.append("--all-namespaces")
|
|
18
|
+
elif args.get("namespace"):
|
|
19
|
+
argv += ["--namespace", args["namespace"]]
|
|
20
|
+
if args.get("output"):
|
|
21
|
+
argv += ["-o", args["output"]]
|
|
22
|
+
return argv
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _describe_args(args: dict) -> list[str]:
|
|
26
|
+
argv = ["describe", args["resource"], args["name"]]
|
|
27
|
+
if args.get("namespace"):
|
|
28
|
+
argv += ["--namespace", args["namespace"]]
|
|
29
|
+
return argv
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _logs_args(args: dict) -> list[str]:
|
|
33
|
+
argv = ["logs", args["pod_name"]]
|
|
34
|
+
if args.get("namespace"):
|
|
35
|
+
argv += ["--namespace", args["namespace"]]
|
|
36
|
+
if args.get("container"):
|
|
37
|
+
argv += ["-c", args["container"]]
|
|
38
|
+
if args.get("tail"):
|
|
39
|
+
argv += ["--tail", str(args["tail"])]
|
|
40
|
+
if args.get("previous"):
|
|
41
|
+
argv.append("--previous")
|
|
42
|
+
return argv
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_contexts_args(_args: dict) -> list[str]:
|
|
46
|
+
return ["config", "get-contexts"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _apply_args(args: dict) -> list[str]:
|
|
50
|
+
argv = ["apply", "-f", args["file"]]
|
|
51
|
+
if args.get("namespace"):
|
|
52
|
+
argv += ["--namespace", args["namespace"]]
|
|
53
|
+
if args.get("dry_run"):
|
|
54
|
+
argv.append("--dry-run=client")
|
|
55
|
+
return argv
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _scale_args(args: dict) -> list[str]:
|
|
59
|
+
argv = ["scale", args["resource"], args["name"], "--replicas", str(args["replicas"])]
|
|
60
|
+
if args.get("namespace"):
|
|
61
|
+
argv += ["--namespace", args["namespace"]]
|
|
62
|
+
return argv
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _delete_args(args: dict) -> list[str]:
|
|
66
|
+
argv = ["delete", args["resource"], args["name"]]
|
|
67
|
+
if args.get("namespace"):
|
|
68
|
+
argv += ["--namespace", args["namespace"]]
|
|
69
|
+
return argv
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class KubectlPlugin(ToolboxPlugin):
|
|
73
|
+
software_name = "kubectl"
|
|
74
|
+
binary_names = ("kubectl",)
|
|
75
|
+
version_args = ("version", "--client")
|
|
76
|
+
|
|
77
|
+
def parse_version(self, version_output: str) -> tuple[int, ...] | None:
|
|
78
|
+
match = _VERSION_RE.search(version_output)
|
|
79
|
+
if not match:
|
|
80
|
+
return None
|
|
81
|
+
return tuple(int(g) for g in match.groups())
|
|
82
|
+
|
|
83
|
+
def build_tools(self, detection: DetectionResult) -> list[CommandSpec]:
|
|
84
|
+
binary = detection.binary_path
|
|
85
|
+
return [
|
|
86
|
+
CommandSpec(
|
|
87
|
+
name="get",
|
|
88
|
+
description="List Kubernetes resources of a given type (pods, deployments, "
|
|
89
|
+
"services, nodes, ...).",
|
|
90
|
+
parameters={
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"resource": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Resource type, e.g. 'pods', 'deployments', 'svc', 'nodes'.",
|
|
96
|
+
},
|
|
97
|
+
"name": {"type": "string", "description": "Specific resource name (optional)."},
|
|
98
|
+
"namespace": {"type": "string"},
|
|
99
|
+
"all_namespaces": {"type": "boolean"},
|
|
100
|
+
"output": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Output format, e.g. 'json', 'yaml', 'wide'.",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"required": ["resource"],
|
|
106
|
+
},
|
|
107
|
+
build_args=_get_args,
|
|
108
|
+
binary_path=binary,
|
|
109
|
+
risk="read",
|
|
110
|
+
),
|
|
111
|
+
CommandSpec(
|
|
112
|
+
name="describe",
|
|
113
|
+
description="Show detailed information about a specific Kubernetes resource.",
|
|
114
|
+
parameters={
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"resource": {"type": "string"},
|
|
118
|
+
"name": {"type": "string"},
|
|
119
|
+
"namespace": {"type": "string"},
|
|
120
|
+
},
|
|
121
|
+
"required": ["resource", "name"],
|
|
122
|
+
},
|
|
123
|
+
build_args=_describe_args,
|
|
124
|
+
binary_path=binary,
|
|
125
|
+
risk="read",
|
|
126
|
+
),
|
|
127
|
+
CommandSpec(
|
|
128
|
+
name="logs",
|
|
129
|
+
description="Fetch logs from a pod (optionally a specific container).",
|
|
130
|
+
parameters={
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"pod_name": {"type": "string"},
|
|
134
|
+
"namespace": {"type": "string"},
|
|
135
|
+
"container": {"type": "string"},
|
|
136
|
+
"tail": {
|
|
137
|
+
"type": "integer",
|
|
138
|
+
"description": "Number of lines from the end of the log to show.",
|
|
139
|
+
},
|
|
140
|
+
"previous": {
|
|
141
|
+
"type": "boolean",
|
|
142
|
+
"description": "Show logs from the previous terminated container instance.",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"required": ["pod_name"],
|
|
146
|
+
},
|
|
147
|
+
build_args=_logs_args,
|
|
148
|
+
binary_path=binary,
|
|
149
|
+
risk="read",
|
|
150
|
+
),
|
|
151
|
+
CommandSpec(
|
|
152
|
+
name="get_contexts",
|
|
153
|
+
description="List available kubeconfig contexts.",
|
|
154
|
+
parameters={"type": "object", "properties": {}},
|
|
155
|
+
build_args=_get_contexts_args,
|
|
156
|
+
binary_path=binary,
|
|
157
|
+
risk="read",
|
|
158
|
+
),
|
|
159
|
+
CommandSpec(
|
|
160
|
+
name="apply",
|
|
161
|
+
description="Apply a Kubernetes manifest file (create/update resources).",
|
|
162
|
+
parameters={
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"file": {"type": "string", "description": "Path to the manifest file."},
|
|
166
|
+
"namespace": {"type": "string"},
|
|
167
|
+
"dry_run": {
|
|
168
|
+
"type": "boolean",
|
|
169
|
+
"description": "Preview the change without applying it.",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
"required": ["file"],
|
|
173
|
+
},
|
|
174
|
+
build_args=_apply_args,
|
|
175
|
+
binary_path=binary,
|
|
176
|
+
risk="mutate",
|
|
177
|
+
),
|
|
178
|
+
CommandSpec(
|
|
179
|
+
name="scale",
|
|
180
|
+
description="Scale a deployment/replicaset/statefulset to a given replica count.",
|
|
181
|
+
parameters={
|
|
182
|
+
"type": "object",
|
|
183
|
+
"properties": {
|
|
184
|
+
"resource": {"type": "string", "description": "e.g. 'deployment'."},
|
|
185
|
+
"name": {"type": "string"},
|
|
186
|
+
"replicas": {"type": "integer"},
|
|
187
|
+
"namespace": {"type": "string"},
|
|
188
|
+
},
|
|
189
|
+
"required": ["resource", "name", "replicas"],
|
|
190
|
+
},
|
|
191
|
+
build_args=_scale_args,
|
|
192
|
+
binary_path=binary,
|
|
193
|
+
risk="mutate",
|
|
194
|
+
),
|
|
195
|
+
CommandSpec(
|
|
196
|
+
name="delete",
|
|
197
|
+
description="Delete a specific Kubernetes resource.",
|
|
198
|
+
parameters={
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {
|
|
201
|
+
"resource": {"type": "string"},
|
|
202
|
+
"name": {"type": "string"},
|
|
203
|
+
"namespace": {"type": "string"},
|
|
204
|
+
},
|
|
205
|
+
"required": ["resource", "name"],
|
|
206
|
+
},
|
|
207
|
+
build_args=_delete_args,
|
|
208
|
+
binary_path=binary,
|
|
209
|
+
risk="destructive",
|
|
210
|
+
),
|
|
211
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Curated plugin for Sun Grid Engine / Univa Grid Engine — unlike kubectl or
|
|
2
|
+
httpd, this is a suite of separate binaries (qstat/qsub/qdel/qacct/qhost),
|
|
3
|
+
so build_tools() resolves each sibling independently and only includes
|
|
4
|
+
commands whose binary is actually present."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
from pcli.tools.toolbox.plugin_base import CommandSpec, DetectionResult, ToolboxPlugin
|
|
12
|
+
|
|
13
|
+
_VERSION_RE = re.compile(r"(\d+)\.(\d+)\.(\d+)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _qstat_args(args: dict) -> list[str]:
|
|
17
|
+
argv = []
|
|
18
|
+
if args.get("all_users"):
|
|
19
|
+
argv += ["-u", args.get("user") or "*"]
|
|
20
|
+
if args.get("job_id"):
|
|
21
|
+
argv += ["-j", str(args["job_id"])]
|
|
22
|
+
if args.get("full"):
|
|
23
|
+
argv.append("-f")
|
|
24
|
+
return argv
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _qhost_args(args: dict) -> list[str]:
|
|
28
|
+
return ["-q"] if args.get("queues") else []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _qacct_args(args: dict) -> list[str]:
|
|
32
|
+
return ["-j", str(args["job_id"])]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _qsub_args(args: dict) -> list[str]:
|
|
36
|
+
argv: list[str] = []
|
|
37
|
+
if args.get("queue"):
|
|
38
|
+
argv += ["-q", args["queue"]]
|
|
39
|
+
if args.get("job_name"):
|
|
40
|
+
argv += ["-N", args["job_name"]]
|
|
41
|
+
argv.append(args["script"])
|
|
42
|
+
return argv
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _qdel_args(args: dict) -> list[str]:
|
|
46
|
+
return [str(args["job_id"])]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SgePlugin(ToolboxPlugin):
|
|
50
|
+
software_name = "sge"
|
|
51
|
+
binary_names = ("qstat",)
|
|
52
|
+
version_args = ("-help",)
|
|
53
|
+
|
|
54
|
+
def parse_version(self, version_output: str) -> tuple[int, ...] | None:
|
|
55
|
+
match = _VERSION_RE.search(version_output)
|
|
56
|
+
if not match:
|
|
57
|
+
# `qstat -help` doesn't reliably print a parseable version across SGE/UGE
|
|
58
|
+
# forks; treat any output at all as "present, version unknown".
|
|
59
|
+
return (0,)
|
|
60
|
+
return tuple(int(g) for g in match.groups())
|
|
61
|
+
|
|
62
|
+
def build_tools(self, detection: DetectionResult) -> list[CommandSpec]:
|
|
63
|
+
specs: list[CommandSpec] = [
|
|
64
|
+
CommandSpec(
|
|
65
|
+
name="qstat",
|
|
66
|
+
description="Show the status of scheduled/running jobs in the grid.",
|
|
67
|
+
parameters={
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"all_users": {"type": "boolean"},
|
|
71
|
+
"user": {"type": "string"},
|
|
72
|
+
"job_id": {"type": "integer"},
|
|
73
|
+
"full": {"type": "boolean", "description": "Full job/queue detail."},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
build_args=_qstat_args,
|
|
77
|
+
binary_path=detection.binary_path,
|
|
78
|
+
risk="read",
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
if qhost := shutil.which("qhost"):
|
|
83
|
+
specs.append(
|
|
84
|
+
CommandSpec(
|
|
85
|
+
name="qhost",
|
|
86
|
+
description="Show the status of execution hosts in the grid.",
|
|
87
|
+
parameters={"type": "object", "properties": {"queues": {"type": "boolean"}}},
|
|
88
|
+
build_args=_qhost_args,
|
|
89
|
+
binary_path=qhost,
|
|
90
|
+
risk="read",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if qacct := shutil.which("qacct"):
|
|
95
|
+
specs.append(
|
|
96
|
+
CommandSpec(
|
|
97
|
+
name="qacct",
|
|
98
|
+
description="Show accounting/history information for a completed job.",
|
|
99
|
+
parameters={
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {"job_id": {"type": "integer"}},
|
|
102
|
+
"required": ["job_id"],
|
|
103
|
+
},
|
|
104
|
+
build_args=_qacct_args,
|
|
105
|
+
binary_path=qacct,
|
|
106
|
+
risk="read",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if qsub := shutil.which("qsub"):
|
|
111
|
+
specs.append(
|
|
112
|
+
CommandSpec(
|
|
113
|
+
name="qsub",
|
|
114
|
+
description="Submit a job script to the grid scheduler.",
|
|
115
|
+
parameters={
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"script": {"type": "string", "description": "Path to the job script."},
|
|
119
|
+
"queue": {"type": "string"},
|
|
120
|
+
"job_name": {"type": "string"},
|
|
121
|
+
},
|
|
122
|
+
"required": ["script"],
|
|
123
|
+
},
|
|
124
|
+
build_args=_qsub_args,
|
|
125
|
+
binary_path=qsub,
|
|
126
|
+
risk="mutate",
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if qdel := shutil.which("qdel"):
|
|
131
|
+
specs.append(
|
|
132
|
+
CommandSpec(
|
|
133
|
+
name="qdel",
|
|
134
|
+
description="Delete/cancel a submitted job.",
|
|
135
|
+
parameters={
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {"job_id": {"type": "integer"}},
|
|
138
|
+
"required": ["job_id"],
|
|
139
|
+
},
|
|
140
|
+
build_args=_qdel_args,
|
|
141
|
+
binary_path=qdel,
|
|
142
|
+
risk="destructive",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return specs
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Persistent cache for toolbox discovery results.
|
|
2
|
+
|
|
3
|
+
Curated plugins re-detect live on every load (a `--version` probe is cheap
|
|
4
|
+
and catches upgrades) — nothing curated needs to be cached. Only the
|
|
5
|
+
LLM-synthesized schemas for uncurated software are cached, keyed by a hash
|
|
6
|
+
of the --help corpus they were generated from, so an unchanged install
|
|
7
|
+
doesn't re-trigger an LLM call on every startup.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from pcli.config.paths import toolbox_dir
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _atomic_write(path: Path, content: str) -> None:
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
23
|
+
tmp_path.write_text(content, encoding="utf-8")
|
|
24
|
+
os.replace(tmp_path, path)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def registry_path() -> Path:
|
|
28
|
+
return toolbox_dir() / "registry.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_registry() -> dict[str, dict]:
|
|
32
|
+
path = registry_path()
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return {}
|
|
35
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_registry(registry: dict[str, dict]) -> None:
|
|
39
|
+
_atomic_write(registry_path(), json.dumps(registry, indent=2))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def synthesized_schema_path(software_name: str) -> Path:
|
|
43
|
+
return toolbox_dir() / software_name / "synthesized.json"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_synthesized_schema(software_name: str) -> dict | None:
|
|
47
|
+
path = synthesized_schema_path(software_name)
|
|
48
|
+
if not path.exists():
|
|
49
|
+
return None
|
|
50
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def write_synthesized_schema(
|
|
54
|
+
software_name: str, *, help_corpus_hash: str, version: str, tools: list[dict]
|
|
55
|
+
) -> None:
|
|
56
|
+
_atomic_write(
|
|
57
|
+
synthesized_schema_path(software_name),
|
|
58
|
+
json.dumps(
|
|
59
|
+
{"help_corpus_hash": help_corpus_hash, "version": version, "tools": tools}, indent=2
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def hash_corpus(text: str) -> str:
|
|
65
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
|