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.
Files changed (130) hide show
  1. pcli/__init__.py +1 -0
  2. pcli/__main__.py +4 -0
  3. pcli/agent/__init__.py +0 -0
  4. pcli/agent/activity.py +116 -0
  5. pcli/agent/compaction.py +205 -0
  6. pcli/agent/context_pruning.py +88 -0
  7. pcli/agent/headless.py +209 -0
  8. pcli/agent/loop.py +442 -0
  9. pcli/agent/prompt.py +371 -0
  10. pcli/agent/runtime.py +240 -0
  11. pcli/browser/__init__.py +0 -0
  12. pcli/browser/session.py +135 -0
  13. pcli/cli.py +757 -0
  14. pcli/config/__init__.py +0 -0
  15. pcli/config/paths.py +95 -0
  16. pcli/config/settings.py +435 -0
  17. pcli/cost/__init__.py +0 -0
  18. pcli/cost/context.py +275 -0
  19. pcli/cost/context_detect.py +183 -0
  20. pcli/cost/pricing_table.py +141 -0
  21. pcli/cost/tracker.py +126 -0
  22. pcli/llm/__init__.py +0 -0
  23. pcli/llm/client.py +285 -0
  24. pcli/llm/errors.py +37 -0
  25. pcli/llm/models.py +100 -0
  26. pcli/llm/streaming.py +108 -0
  27. pcli/memory/__init__.py +0 -0
  28. pcli/memory/extraction.py +106 -0
  29. pcli/memory/models.py +103 -0
  30. pcli/memory/store.py +88 -0
  31. pcli/permissions/__init__.py +0 -0
  32. pcli/permissions/guardrails.py +219 -0
  33. pcli/permissions/manager.py +215 -0
  34. pcli/permissions/policy.py +70 -0
  35. pcli/sandbox/__init__.py +0 -0
  36. pcli/sandbox/base.py +50 -0
  37. pcli/sandbox/docker_backend.py +107 -0
  38. pcli/sandbox/limits.py +63 -0
  39. pcli/sandbox/null_backend.py +92 -0
  40. pcli/sandbox/selector.py +75 -0
  41. pcli/sandbox/subprocess_backend.py +376 -0
  42. pcli/scheduler/__init__.py +0 -0
  43. pcli/scheduler/daemon.py +194 -0
  44. pcli/scheduler/models.py +97 -0
  45. pcli/scheduler/runner.py +84 -0
  46. pcli/scheduler/store.py +75 -0
  47. pcli/scheduler/triggers.py +84 -0
  48. pcli/session/__init__.py +0 -0
  49. pcli/session/audit.py +122 -0
  50. pcli/session/directory_check.py +28 -0
  51. pcli/session/export.py +57 -0
  52. pcli/session/importer.py +92 -0
  53. pcli/session/models.py +168 -0
  54. pcli/session/store.py +127 -0
  55. pcli/telegram/__init__.py +0 -0
  56. pcli/telegram/bot.py +266 -0
  57. pcli/telegram/daemon.py +1197 -0
  58. pcli/telegram/permissions.py +131 -0
  59. pcli/telegram/sender.py +58 -0
  60. pcli/tools/__init__.py +0 -0
  61. pcli/tools/_nested_agent.py +204 -0
  62. pcli/tools/agent_tools.py +264 -0
  63. pcli/tools/agent_tools_store.py +69 -0
  64. pcli/tools/artifacts.py +47 -0
  65. pcli/tools/base.py +185 -0
  66. pcli/tools/builtin/__init__.py +0 -0
  67. pcli/tools/builtin/agent_tool_register_tool.py +100 -0
  68. pcli/tools/builtin/artifact_tool.py +212 -0
  69. pcli/tools/builtin/ask_tool.py +77 -0
  70. pcli/tools/builtin/browser_tool.py +253 -0
  71. pcli/tools/builtin/decision_tool.py +73 -0
  72. pcli/tools/builtin/describe_tool.py +389 -0
  73. pcli/tools/builtin/diff_tools.py +225 -0
  74. pcli/tools/builtin/fs_tools.py +371 -0
  75. pcli/tools/builtin/grep_tool.py +88 -0
  76. pcli/tools/builtin/memory_tool.py +108 -0
  77. pcli/tools/builtin/network_tools.py +107 -0
  78. pcli/tools/builtin/pip_tool.py +106 -0
  79. pcli/tools/builtin/shell_tool.py +240 -0
  80. pcli/tools/builtin/subagent_tool.py +146 -0
  81. pcli/tools/builtin/todo_tool.py +122 -0
  82. pcli/tools/builtin/toolbox_register_tool.py +76 -0
  83. pcli/tools/builtin/web_tools.py +322 -0
  84. pcli/tools/pydiscovery/__init__.py +0 -0
  85. pcli/tools/pydiscovery/cache.py +51 -0
  86. pcli/tools/pydiscovery/index.py +48 -0
  87. pcli/tools/pydiscovery/invoke.py +181 -0
  88. pcli/tools/pydiscovery/search.py +117 -0
  89. pcli/tools/registry.py +138 -0
  90. pcli/tools/toolbox/__init__.py +0 -0
  91. pcli/tools/toolbox/introspect.py +48 -0
  92. pcli/tools/toolbox/manager.py +336 -0
  93. pcli/tools/toolbox/plugin_base.py +51 -0
  94. pcli/tools/toolbox/plugins/__init__.py +6 -0
  95. pcli/tools/toolbox/plugins/httpd.py +99 -0
  96. pcli/tools/toolbox/plugins/kafka.py +162 -0
  97. pcli/tools/toolbox/plugins/kubectl.py +211 -0
  98. pcli/tools/toolbox/plugins/sge.py +146 -0
  99. pcli/tools/toolbox/store.py +65 -0
  100. pcli/tools/toolbox/synthesize.py +100 -0
  101. pcli/tui/__init__.py +0 -0
  102. pcli/tui/app.py +37 -0
  103. pcli/tui/screens/__init__.py +0 -0
  104. pcli/tui/screens/ask_question_modal.py +54 -0
  105. pcli/tui/screens/chat.py +2070 -0
  106. pcli/tui/screens/confirm_modal.py +39 -0
  107. pcli/tui/screens/models.py +43 -0
  108. pcli/tui/screens/permission_modal.py +71 -0
  109. pcli/tui/screens/sessions.py +162 -0
  110. pcli/tui/screens/subagent_activity_modal.py +71 -0
  111. pcli/tui/shell_passthrough.py +56 -0
  112. pcli/tui/styles/pcli.tcss +241 -0
  113. pcli/tui/themes.py +84 -0
  114. pcli/tui/widgets/__init__.py +0 -0
  115. pcli/tui/widgets/chat_input.py +240 -0
  116. pcli/tui/widgets/command_suggestions.py +33 -0
  117. pcli/tui/widgets/message_view.py +328 -0
  118. pcli/tui/widgets/paste_input.py +99 -0
  119. pcli/tui/widgets/paste_marker.py +69 -0
  120. pcli/tui/widgets/status_bar.py +133 -0
  121. pcli/tui/widgets/status_pane.py +58 -0
  122. pcli/util/__init__.py +0 -0
  123. pcli/util/ids.py +15 -0
  124. pcli/util/logging.py +18 -0
  125. pcli/util/text.py +10 -0
  126. pcli_agent-0.1.0.dist-info/METADATA +259 -0
  127. pcli_agent-0.1.0.dist-info/RECORD +130 -0
  128. pcli_agent-0.1.0.dist-info/WHEEL +4 -0
  129. pcli_agent-0.1.0.dist-info/entry_points.txt +2 -0
  130. 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))