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,100 @@
|
|
|
1
|
+
"""LLM-driven tool schema synthesis from raw --help text, for software
|
|
2
|
+
without a curated plugin. Strictly schema-validated before being trusted;
|
|
3
|
+
retries once with the validation error fed back if the first attempt
|
|
4
|
+
doesn't parse/validate, then gives up rather than registering something
|
|
5
|
+
malformed."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import jsonschema
|
|
12
|
+
|
|
13
|
+
from pcli.llm.client import GatewayClient
|
|
14
|
+
from pcli.llm.models import ChatMessage
|
|
15
|
+
|
|
16
|
+
SYNTHESIZED_TOOL_SCHEMA = {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"tools": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"name": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"},
|
|
25
|
+
"description": {"type": "string"},
|
|
26
|
+
"subcommand": {"type": "array", "items": {"type": "string"}},
|
|
27
|
+
"parameters": {"type": "object"},
|
|
28
|
+
"risk": {"type": "string", "enum": ["read", "mutate", "destructive"]},
|
|
29
|
+
},
|
|
30
|
+
"required": ["name", "description", "subcommand", "parameters", "risk"],
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": ["tools"],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_SYSTEM_PROMPT = """You turn raw CLI --help output into a JSON list of tool definitions for \
|
|
38
|
+
an AI agent to call. Rules:
|
|
39
|
+
- Each tool maps to ONE specific subcommand invocation, given as an argv list in \
|
|
40
|
+
"subcommand" (e.g. ["get", "pods"], or [] if the binary takes flags directly with no \
|
|
41
|
+
subcommand).
|
|
42
|
+
- "parameters" is a JSON Schema object (with "type": "object" and "properties") describing \
|
|
43
|
+
the subcommand's *optional/value flags* as named properties (e.g. "namespace", "output"), \
|
|
44
|
+
NOT positional words already baked into "subcommand". Use "type": "boolean" for flags that \
|
|
45
|
+
take no value. Property names must be valid identifiers (letters, digits, underscore).
|
|
46
|
+
- "risk" must be "read" for read-only/inspection commands, "mutate" for commands that change \
|
|
47
|
+
state, "destructive" for commands that delete/terminate/irreversibly change state. When in \
|
|
48
|
+
doubt, choose the more cautious (destructive over mutate, mutate over read).
|
|
49
|
+
- Only include commands you are reasonably confident about from the help text. Fewer, \
|
|
50
|
+
correct tools are better than many guessed ones.
|
|
51
|
+
- Respond with ONLY a JSON object of the shape {"tools": [...]}, no prose, no code fences."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _strip_code_fence(text: str) -> str:
|
|
55
|
+
if not text.startswith("```"):
|
|
56
|
+
return text
|
|
57
|
+
lines = text.splitlines()
|
|
58
|
+
if lines and lines[0].startswith("```"):
|
|
59
|
+
lines = lines[1:]
|
|
60
|
+
if lines and lines[-1].strip() == "```":
|
|
61
|
+
lines = lines[:-1]
|
|
62
|
+
return "\n".join(lines)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def synthesize_tools(
|
|
66
|
+
client: GatewayClient,
|
|
67
|
+
*,
|
|
68
|
+
software_name: str,
|
|
69
|
+
help_corpus: str,
|
|
70
|
+
model: str | None = None,
|
|
71
|
+
) -> list[dict]:
|
|
72
|
+
messages = [
|
|
73
|
+
ChatMessage(role="system", content=_SYSTEM_PROMPT),
|
|
74
|
+
ChatMessage(
|
|
75
|
+
role="user",
|
|
76
|
+
content=f"Software: {software_name}\n\n--help output:\n\n{help_corpus[:12000]}",
|
|
77
|
+
),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
last_error: Exception | None = None
|
|
81
|
+
for attempt in range(2):
|
|
82
|
+
message, _usage = await client.collect(messages, model=model)
|
|
83
|
+
raw = _strip_code_fence((message.content or "").strip())
|
|
84
|
+
try:
|
|
85
|
+
data = json.loads(raw)
|
|
86
|
+
jsonschema.validate(data, SYNTHESIZED_TOOL_SCHEMA)
|
|
87
|
+
return data["tools"]
|
|
88
|
+
except (json.JSONDecodeError, jsonschema.ValidationError) as exc:
|
|
89
|
+
last_error = exc
|
|
90
|
+
if attempt == 0:
|
|
91
|
+
messages.append(ChatMessage(role="assistant", content=raw))
|
|
92
|
+
messages.append(
|
|
93
|
+
ChatMessage(
|
|
94
|
+
role="user",
|
|
95
|
+
content=f"That wasn't valid: {exc}. Respond again with ONLY the "
|
|
96
|
+
"corrected JSON object.",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
raise ValueError(f"Could not synthesize a valid tool schema for '{software_name}': {last_error}")
|
pcli/tui/__init__.py
ADDED
|
File without changes
|
pcli/tui/app.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Root Textual application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual.app import App
|
|
8
|
+
|
|
9
|
+
from pcli.config.settings import Settings, get_settings
|
|
10
|
+
from pcli.session.models import Session
|
|
11
|
+
from pcli.tui.screens.chat import ChatScreen
|
|
12
|
+
from pcli.tui.themes import VIM_THEMES
|
|
13
|
+
|
|
14
|
+
_STYLES_PATH = Path(__file__).parent / "styles" / "pcli.tcss"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PcliApp(App):
|
|
18
|
+
CSS_PATH = str(_STYLES_PATH)
|
|
19
|
+
TITLE = "pcli"
|
|
20
|
+
|
|
21
|
+
def __init__(self, settings: Settings | None = None, *, session: Session | None = None) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._settings = settings or get_settings()
|
|
24
|
+
self._initial_session = session
|
|
25
|
+
|
|
26
|
+
def on_mount(self) -> None:
|
|
27
|
+
for theme in VIM_THEMES:
|
|
28
|
+
self.register_theme(theme)
|
|
29
|
+
# settings.ui_theme is free-text (hand-edited config.toml, or a
|
|
30
|
+
# stale value from a pcli version whose theme set has since
|
|
31
|
+
# changed) - applying an unknown name would raise straight out of
|
|
32
|
+
# on_mount, so this is checked rather than trusted. Falls back to
|
|
33
|
+
# whatever App.theme already defaults to (textual-dark) rather than
|
|
34
|
+
# failing the whole app over one bad setting.
|
|
35
|
+
if self._settings.ui_theme in self.available_themes:
|
|
36
|
+
self.theme = self._settings.ui_theme
|
|
37
|
+
self.push_screen(ChatScreen(self._settings, session=self._initial_session))
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Modal that implements the ask_user_question tool's ask_question
|
|
2
|
+
callback via Textual — lets the model pause a turn to ask the user
|
|
3
|
+
something directly, optionally with a fixed set of suggested answers,
|
|
4
|
+
mirroring opencode's "ask" tool. A free-text field is always available
|
|
5
|
+
regardless of whether options are given, so a suggested answer never
|
|
6
|
+
forecloses a different one."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
13
|
+
from textual.containers import Vertical, VerticalScroll
|
|
14
|
+
from textual.screen import ModalScreen
|
|
15
|
+
from textual.widgets import Button, Input, Static
|
|
16
|
+
|
|
17
|
+
from pcli.tui.widgets.paste_input import PasteInput
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AskQuestionModal(ModalScreen[str]):
|
|
21
|
+
def __init__(self, question: str, options: list[str] | None = None) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._question = question
|
|
24
|
+
self._options = options or []
|
|
25
|
+
|
|
26
|
+
def compose(self) -> ComposeResult:
|
|
27
|
+
with VerticalScroll(id="ask-question-modal"):
|
|
28
|
+
yield Static(self._question, id="ask-question-text")
|
|
29
|
+
if self._options:
|
|
30
|
+
with Vertical(id="ask-question-options"):
|
|
31
|
+
for index, option in enumerate(self._options):
|
|
32
|
+
yield Button(option, id=f"ask-question-option-{index}")
|
|
33
|
+
yield PasteInput(
|
|
34
|
+
placeholder="Or type your own answer and press Enter...",
|
|
35
|
+
id="ask-question-input",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def on_mount(self) -> None:
|
|
39
|
+
self.query_one(PasteInput).focus()
|
|
40
|
+
|
|
41
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
42
|
+
for index, option in enumerate(self._options):
|
|
43
|
+
if event.button.id == f"ask-question-option-{index}":
|
|
44
|
+
self.dismiss(option)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
48
|
+
answer = event.value.strip()
|
|
49
|
+
if answer:
|
|
50
|
+
self.dismiss(answer)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def ask_question_via_modal(app: App[Any], question: str, options: list[str] | None) -> str:
|
|
54
|
+
return await app.push_screen_wait(AskQuestionModal(question, options))
|