termagent-cli 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- termagent_cli-0.3.0/PKG-INFO +38 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/README.md +8 -19
- termagent_cli-0.3.0/package_info.md +22 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/pyproject.toml +2 -1
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent/agent/graph.py +19 -5
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent/agent/nodes.py +62 -31
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent/agent/state.py +3 -1
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent/ui.py +121 -30
- termagent_cli-0.3.0/termagent/viz.py +4 -0
- termagent_cli-0.3.0/termagent_cli.egg-info/PKG-INFO +38 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent_cli.egg-info/SOURCES.txt +2 -0
- termagent_cli-0.2.0/PKG-INFO +0 -14
- termagent_cli-0.2.0/termagent_cli.egg-info/PKG-INFO +0 -14
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/setup.cfg +0 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent/__init__.py +0 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent_cli.egg-info/dependency_links.txt +0 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent_cli.egg-info/entry_points.txt +0 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent_cli.egg-info/requires.txt +0 -0
- {termagent_cli-0.2.0 → termagent_cli-0.3.0}/termagent_cli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: termagent-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Natural language terminal agent for Windows PowerShell
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: langchain-groq
|
|
8
|
+
Requires-Dist: langchain-core
|
|
9
|
+
Requires-Dist: langchain
|
|
10
|
+
Requires-Dist: langgraph
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
13
|
+
Requires-Dist: ollama
|
|
14
|
+
Requires-Dist: textual
|
|
15
|
+
Requires-Dist: rich
|
|
16
|
+
|
|
17
|
+
# Termagent: Natural Language PowerShell Agent
|
|
18
|
+
|
|
19
|
+
Termagent is a powerful, Windows-native AI agent that transforms natural language instructions into executable PowerShell commands. Built for both non-technical users and power developers, it bridges the gap between intent and execution through a sophisticated agentic workflow.
|
|
20
|
+
|
|
21
|
+
## 🚀 Key Features
|
|
22
|
+
|
|
23
|
+
- **Natural Language Translation**: Convert plain English into complex PowerShell scripts instantly.
|
|
24
|
+
- **Stateful Orchestration**: Built on **LangGraph**, allowing the agent to remember context, navigate directories, and handle multi-step operations.
|
|
25
|
+
- **Dual-Layer Safety Architecture**:
|
|
26
|
+
- **Static Blacklist**: Prevents system-critical operations (e.g., System32 access, Registry edits) before they reach the LLM.
|
|
27
|
+
- **LLM Security Review**: A second layer of reasoning that flags context-specific risks.
|
|
28
|
+
- **Human-in-the-Loop (HITL)**: Risky commands are never executed without explicit user confirmation.
|
|
29
|
+
- **Textual TUI**: A beautiful, responsive terminal user interface for a premium CLI experience.
|
|
30
|
+
- **Email Integration**: Compose and send professional emails with attachments directly from the terminal.
|
|
31
|
+
|
|
32
|
+
## 🛠️ Architecture
|
|
33
|
+
|
|
34
|
+
Termagent utilizes a Directed Acyclic Graph (DAG) state machine to manage the conversation flow, tool selection, and command validation. By running inference on **Groq**, it ensures near-instant response times while maintaining high reasoning quality.
|
|
35
|
+
|
|
36
|
+
## ⚠️ Safety First
|
|
37
|
+
|
|
38
|
+
While Termagent is designed to be helpful, it executes real commands on your system. Always review flagged commands before confirming execution.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
---
|
|
19
|
-

|
|
20
20
|
|
|
21
21
|
<br/>
|
|
22
22
|
|
|
@@ -102,6 +102,12 @@ Navigate freely — the agent always knows where you are.
|
|
|
102
102
|
|
|
103
103
|
<br/>
|
|
104
104
|
|
|
105
|
+
## 📐 Agent Architecture
|
|
106
|
+
|
|
107
|
+
Termagent is built on **[LangGraph](https://github.com/langchain-ai/langgraph)** — a stateful agent framework. The pipeline is a directed graph:
|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
|
|
105
111
|
## 💾 Installation
|
|
106
112
|
|
|
107
113
|
### Requirements
|
|
@@ -131,29 +137,12 @@ python -m termagent
|
|
|
131
137
|
| Variable | Description |
|
|
132
138
|
|:---:|---|
|
|
133
139
|
| `GROQ_API_KEY` | Your Groq API key (prompted on first run) |
|
|
134
|
-
| `EMAIL_PASSWORD` | Get App Password for your gmail account [here](
|
|
140
|
+
| `EMAIL_PASSWORD` | Get App Password for your gmail account [here](myaccount.google.com/apppasswords) (prompted on first run) |
|
|
135
141
|
|
|
136
142
|
*Termagent saves your key, passwords and email address to a local `.env` file on first run — you won't be asked again.*
|
|
137
143
|
|
|
138
144
|
<br/>
|
|
139
145
|
|
|
140
|
-
## 📐 Architecture
|
|
141
|
-
|
|
142
|
-
Termagent is built on **[LangGraph](https://github.com/langchain-ai/langgraph)** — a stateful agent framework. The pipeline is a directed graph:
|
|
143
|
-
|
|
144
|
-
```mermaid
|
|
145
|
-
flowchart TD
|
|
146
|
-
A["__start__"] --> B["generate_command"]
|
|
147
|
-
B -->|"intent: chat"| C["chat_node"]
|
|
148
|
-
B -->|"intent: command"| D["check_command"]
|
|
149
|
-
D -->|"safe"| F["execute_command"]
|
|
150
|
-
D -->|"risky"| E["confirm_command"]
|
|
151
|
-
E -->|"execute"| F["execute_command"]
|
|
152
|
-
E -->|"do_not_execute"| G["__end__"]
|
|
153
|
-
C --> G
|
|
154
|
-
F --> G
|
|
155
|
-
```
|
|
156
|
-
|
|
157
146
|
### 🛠️ Tech Stack
|
|
158
147
|
- **LangGraph** — agent orchestration
|
|
159
148
|
- **Groq** — LLM inference
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Termagent: Natural Language PowerShell Agent
|
|
2
|
+
|
|
3
|
+
Termagent is a powerful, Windows-native AI agent that transforms natural language instructions into executable PowerShell commands. Built for both non-technical users and power developers, it bridges the gap between intent and execution through a sophisticated agentic workflow.
|
|
4
|
+
|
|
5
|
+
## 🚀 Key Features
|
|
6
|
+
|
|
7
|
+
- **Natural Language Translation**: Convert plain English into complex PowerShell scripts instantly.
|
|
8
|
+
- **Stateful Orchestration**: Built on **LangGraph**, allowing the agent to remember context, navigate directories, and handle multi-step operations.
|
|
9
|
+
- **Dual-Layer Safety Architecture**:
|
|
10
|
+
- **Static Blacklist**: Prevents system-critical operations (e.g., System32 access, Registry edits) before they reach the LLM.
|
|
11
|
+
- **LLM Security Review**: A second layer of reasoning that flags context-specific risks.
|
|
12
|
+
- **Human-in-the-Loop (HITL)**: Risky commands are never executed without explicit user confirmation.
|
|
13
|
+
- **Textual TUI**: A beautiful, responsive terminal user interface for a premium CLI experience.
|
|
14
|
+
- **Email Integration**: Compose and send professional emails with attachments directly from the terminal.
|
|
15
|
+
|
|
16
|
+
## 🛠️ Architecture
|
|
17
|
+
|
|
18
|
+
Termagent utilizes a Directed Acyclic Graph (DAG) state machine to manage the conversation flow, tool selection, and command validation. By running inference on **Groq**, it ensures near-instant response times while maintaining high reasoning quality.
|
|
19
|
+
|
|
20
|
+
## ⚠️ Safety First
|
|
21
|
+
|
|
22
|
+
While Termagent is designed to be helpful, it executes real commands on your system. Always review flagged commands before confirming execution.
|
|
@@ -8,8 +8,9 @@ include = ["termagent*"]
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "termagent-cli"
|
|
11
|
-
version = "0.
|
|
11
|
+
version = "0.3.0"
|
|
12
12
|
description = "Natural language terminal agent for Windows PowerShell"
|
|
13
|
+
readme = "package_info.md"
|
|
13
14
|
requires-python = ">=3.10"
|
|
14
15
|
dependencies = [
|
|
15
16
|
"langchain-groq",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from langgraph.graph import START, END, StateGraph
|
|
2
|
-
from .nodes import generate_command, check_command, confirm_command, execute_command, chat_node,email_node
|
|
2
|
+
from .nodes import generate_command, check_command, confirm_command, execute_command, chat_node, email_node, pre_check, generate_email
|
|
3
3
|
from .state import AgentState
|
|
4
4
|
|
|
5
5
|
def if_risky(state: AgentState) -> str:
|
|
@@ -15,38 +15,52 @@ def ask_user(state: AgentState) -> str:
|
|
|
15
15
|
else:
|
|
16
16
|
return "do_not_execute"
|
|
17
17
|
|
|
18
|
+
def route_pre_check(state: AgentState) -> str:
|
|
19
|
+
if state.get("early_exit", False):
|
|
20
|
+
return "end"
|
|
21
|
+
return "generate_command"
|
|
18
22
|
|
|
19
23
|
def route_intent(state: AgentState) -> str:
|
|
20
24
|
if state["intent"] == "command":
|
|
21
25
|
return "check_command"
|
|
22
26
|
elif state["intent"] == "email":
|
|
23
|
-
return "
|
|
27
|
+
return "generate_email"
|
|
24
28
|
else:
|
|
25
29
|
return "chat_node"
|
|
26
30
|
|
|
27
31
|
graph = StateGraph(AgentState)
|
|
28
32
|
|
|
33
|
+
graph.add_node("pre_check", pre_check)
|
|
29
34
|
graph.add_node("generate_command", generate_command)
|
|
35
|
+
graph.add_node("generate_email", generate_email)
|
|
30
36
|
graph.add_node("chat_node", chat_node)
|
|
31
37
|
graph.add_node("email_node", email_node)
|
|
32
38
|
graph.add_node("check_command", check_command)
|
|
33
39
|
graph.add_node("confirm_command", confirm_command)
|
|
34
40
|
graph.add_node("execute_command", execute_command)
|
|
35
41
|
|
|
42
|
+
graph.add_edge(START, "pre_check")
|
|
43
|
+
graph.add_conditional_edges(
|
|
44
|
+
"pre_check",
|
|
45
|
+
route_pre_check,
|
|
46
|
+
{
|
|
47
|
+
"end": END,
|
|
48
|
+
"generate_command": "generate_command"
|
|
49
|
+
}
|
|
50
|
+
)
|
|
36
51
|
|
|
37
|
-
graph.add_edge(START, "generate_command")
|
|
38
|
-
# graph.add_edge("generate_command", "check_command")
|
|
39
52
|
graph.add_conditional_edges(
|
|
40
53
|
"generate_command",
|
|
41
54
|
route_intent,
|
|
42
55
|
{
|
|
43
56
|
"check_command": "check_command",
|
|
44
57
|
"chat_node": "chat_node",
|
|
45
|
-
"
|
|
58
|
+
"generate_email": "generate_email"
|
|
46
59
|
}
|
|
47
60
|
)
|
|
48
61
|
|
|
49
62
|
graph.add_edge("chat_node", END)
|
|
63
|
+
graph.add_edge("generate_email", "email_node")
|
|
50
64
|
graph.add_edge("email_node", END)
|
|
51
65
|
|
|
52
66
|
graph.add_conditional_edges(
|
|
@@ -66,13 +66,33 @@ class EmailOutput(BaseModel):
|
|
|
66
66
|
recipient: str = Field(..., description="The recipent of the email")
|
|
67
67
|
subject: str = Field(..., description="The subject of the email")
|
|
68
68
|
body: str = Field(..., description="The body of the email")
|
|
69
|
-
attachment: Optional[str] = Field(None, description="The attachment of the email")
|
|
69
|
+
attachment: Optional[list[str]] = Field(None, description="The attachment of the email")
|
|
70
70
|
|
|
71
71
|
class CommandOutput(BaseModel):
|
|
72
72
|
intent: Literal["command", "chat", "email"] = Field(..., description="Whether the user request is to execute a command, send an email or just a casual chat")
|
|
73
73
|
cmd: str = Field("", description="The PowerShell command to execute, if intent is 'command'")
|
|
74
74
|
response: str = Field("", description="The response to return to the user, if intent is 'chat'")
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
def generate_email(state: AgentState) -> AgentState:
|
|
77
|
+
messages = [
|
|
78
|
+
SystemMessage(content="""
|
|
79
|
+
You are an expert email composer. Generate a professional email based on the user's request.
|
|
80
|
+
|
|
81
|
+
RULES:
|
|
82
|
+
- Use a proper greeting, clear body paragraphs, and a sign-off.
|
|
83
|
+
- Sign off using the name provided in "User's name". Never use placeholders like [Your Name].
|
|
84
|
+
- After the sign-off, add a new line: "Sent via Termagent."
|
|
85
|
+
- If an attachment is mentioned, populate the attachment field with the filename.
|
|
86
|
+
- Keep the tone professional unless the user specifies otherwise.
|
|
87
|
+
"""),
|
|
88
|
+
HumanMessage(content=f"User's name: {state.get('user_name', 'User')}\nUser request: {state['text']}")
|
|
89
|
+
]
|
|
90
|
+
llm = ChatGroq(model="llama-3.3-70b-versatile")
|
|
91
|
+
model = llm.with_structured_output(EmailOutput)
|
|
92
|
+
response = model.invoke(messages)
|
|
93
|
+
return {
|
|
94
|
+
"email": response.model_dump() if response else None
|
|
95
|
+
}
|
|
76
96
|
|
|
77
97
|
def generate_command(state: AgentState) -> str:
|
|
78
98
|
|
|
@@ -91,20 +111,13 @@ def generate_command(state: AgentState) -> str:
|
|
|
91
111
|
- No explanations, no markdown, no backticks
|
|
92
112
|
- If the intent is "chat", return an empty string for cmd and provide the answer in response
|
|
93
113
|
- If the intent is "command", provide the PowerShell command in cmd and leave response empty
|
|
94
|
-
|
|
95
|
-
Add a disclaimer at the end of the body that the email is sent by 'Termagent'. Leave cmd and response empty.
|
|
96
|
-
- When composing email bodies, sign off with the user's actual name provided in "User's name", never use placeholders like [your name].
|
|
114
|
+
|
|
97
115
|
PREFERRED CMDLETS:
|
|
98
116
|
- Files/Folders: New-Item, Remove-Item, Copy-Item, Move-Item, Rename-Item, Get-ChildItem
|
|
99
117
|
- Read/Write: Set-Content, Get-Content, Add-Content
|
|
100
118
|
- Info: Get-Location, Get-Process, Get-Service, ipconfig, whoami
|
|
101
119
|
|
|
102
120
|
EXAMPLES:
|
|
103
|
-
User: create a folder named project
|
|
104
|
-
intent: command
|
|
105
|
-
cmd: New-Item -ItemType Directory -Name "project"
|
|
106
|
-
response: ""
|
|
107
|
-
|
|
108
121
|
User: write "hello world" to notes.txt
|
|
109
122
|
intent: command
|
|
110
123
|
cmd: Set-Content -Path "notes.txt" -Value "hello world"
|
|
@@ -119,17 +132,6 @@ def generate_command(state: AgentState) -> str:
|
|
|
119
132
|
intent: command
|
|
120
133
|
cmd: New-Item -ItemType File -Name "readme.txt" -Force; Set-Content -Path "readme.txt" -Value "hello world"
|
|
121
134
|
response: ""
|
|
122
|
-
|
|
123
|
-
User: send report.pdf to john@gmail.com
|
|
124
|
-
intent: email
|
|
125
|
-
cmd: ""
|
|
126
|
-
response: ""
|
|
127
|
-
email: {
|
|
128
|
-
"recipient": "john@gmail.com",
|
|
129
|
-
"subject": "Report",
|
|
130
|
-
"body": "Please find the attached report.",
|
|
131
|
-
"attachment": "report.pdf"
|
|
132
|
-
}
|
|
133
135
|
"""),
|
|
134
136
|
HumanMessage(content=f"Current working directory: {state['cwd']}\nUser's name: {state.get('user_name', 'User')}\nUser request: {state['text']}")
|
|
135
137
|
]
|
|
@@ -143,8 +145,7 @@ def generate_command(state: AgentState) -> str:
|
|
|
143
145
|
return {
|
|
144
146
|
"cmd": response.cmd,
|
|
145
147
|
"intent": response.intent,
|
|
146
|
-
"response": response.response
|
|
147
|
-
"email": response.email.model_dump() if response.email else None
|
|
148
|
+
"response": response.response
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
def chat_node(state: AgentState) -> AgentState:
|
|
@@ -162,7 +163,7 @@ def check_command(state: AgentState) -> AgentState:
|
|
|
162
163
|
]
|
|
163
164
|
|
|
164
165
|
llm = ChatGroq(model="llama-3.3-70b-versatile")
|
|
165
|
-
|
|
166
|
+
|
|
166
167
|
model = llm.with_structured_output(safety_check)
|
|
167
168
|
|
|
168
169
|
response = model.invoke(messages)
|
|
@@ -217,6 +218,9 @@ def email_node(state: AgentState) -> AgentState:
|
|
|
217
218
|
sender = os.getenv("EMAIL_ADDRESS")
|
|
218
219
|
password = os.getenv("EMAIL_PASSWORD")
|
|
219
220
|
|
|
221
|
+
if not sender or not password:
|
|
222
|
+
return {"result": "EMAIL_SETUP_REQUIRED"}
|
|
223
|
+
|
|
220
224
|
try:
|
|
221
225
|
msg = MIMEMultipart()
|
|
222
226
|
msg['From'] = sender
|
|
@@ -226,11 +230,12 @@ def email_node(state: AgentState) -> AgentState:
|
|
|
226
230
|
|
|
227
231
|
# Attach file if provided
|
|
228
232
|
if email_data.get('attachment'):
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
for file in email_data['attachment']:
|
|
234
|
+
attachment_path = os.path.join(state['cwd'], file)
|
|
235
|
+
with open(attachment_path, 'rb') as f:
|
|
236
|
+
part = MIMEBase('application', 'octet-stream')
|
|
237
|
+
part.set_payload(f.read())
|
|
238
|
+
encoders.encode_base64(part)
|
|
234
239
|
part.add_header(
|
|
235
240
|
'Content-Disposition',
|
|
236
241
|
f'attachment; filename={os.path.basename(attachment_path)}'
|
|
@@ -238,7 +243,10 @@ def email_node(state: AgentState) -> AgentState:
|
|
|
238
243
|
msg.attach(part)
|
|
239
244
|
|
|
240
245
|
# Send via Gmail SMTP
|
|
241
|
-
with smtplib.
|
|
246
|
+
with smtplib.SMTP('smtp.gmail.com', 587) as server:
|
|
247
|
+
server.ehlo()
|
|
248
|
+
server.starttls()
|
|
249
|
+
server.ehlo()
|
|
242
250
|
server.login(sender, password)
|
|
243
251
|
server.sendmail(sender, email_data['recipient'], msg.as_string())
|
|
244
252
|
|
|
@@ -249,4 +257,27 @@ def email_node(state: AgentState) -> AgentState:
|
|
|
249
257
|
except smtplib.SMTPAuthenticationError:
|
|
250
258
|
return {"result": "Error: Email authentication failed. Check your EMAIL_ADDRESS and EMAIL_PASSWORD in .env"}
|
|
251
259
|
except Exception as e:
|
|
252
|
-
return {"result": f"Error sending email: {str(e)}"}
|
|
260
|
+
return {"result": f"Error sending email: {str(e)}"}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Keywords that indicate user wants to send an email
|
|
264
|
+
EMAIL_KEYWORDS = [
|
|
265
|
+
"send email", "send an email", "send a email",
|
|
266
|
+
"send mail", "send a mail", "send an mail",
|
|
267
|
+
"email to", "mail to", "mail", "email", "e-mail", "e-mail to",
|
|
268
|
+
"compose email", "compose a mail", "compose an email",
|
|
269
|
+
"write email", "write a mail", "write an email",
|
|
270
|
+
"draft email", "draft a mail", "draft an email",
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
def pre_check(state: AgentState) -> AgentState:
|
|
274
|
+
"""Check if the user is requesting email functionality before invoking the LLM.
|
|
275
|
+
If email keywords are detected and email is not enabled, short-circuit immediately.
|
|
276
|
+
"""
|
|
277
|
+
text_lower = state["text"].lower()
|
|
278
|
+
is_email_request = any(kw in text_lower for kw in EMAIL_KEYWORDS)
|
|
279
|
+
|
|
280
|
+
if is_email_request and not state.get("email_enabled", False):
|
|
281
|
+
return {"result": "EMAIL_SETUP_REQUIRED", "early_exit": True}
|
|
282
|
+
|
|
283
|
+
return {"early_exit": False}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
1
3
|
from textual.app import App, ComposeResult
|
|
2
4
|
from textual.widgets import Input, RichLog, Static, Footer
|
|
3
5
|
from textual.containers import Vertical, Horizontal
|
|
@@ -140,7 +142,10 @@ class TermAgent(App):
|
|
|
140
142
|
|
|
141
143
|
with Horizontal(id="input-container"):
|
|
142
144
|
yield Static("❯", id="prompt-label")
|
|
143
|
-
yield Input(
|
|
145
|
+
yield Input(
|
|
146
|
+
placeholder="Ask anything, or !cmd for raw PowerShell...",
|
|
147
|
+
id="user-input"
|
|
148
|
+
)
|
|
144
149
|
|
|
145
150
|
yield Footer()
|
|
146
151
|
|
|
@@ -151,7 +156,9 @@ class TermAgent(App):
|
|
|
151
156
|
self.update_cwd_label()
|
|
152
157
|
log = self.query_one("#output-log", RichLog)
|
|
153
158
|
log.write(Text.from_markup(
|
|
154
|
-
"[dim]Type a command in plain English or ask a question.
|
|
159
|
+
"[dim]Type a command in plain English or ask a question. "
|
|
160
|
+
"Prefix with [bold magenta]![/bold magenta] to run raw PowerShell directly. "
|
|
161
|
+
"Type [bold cyan]bye[/bold cyan] to exit.[/dim]\n"
|
|
155
162
|
))
|
|
156
163
|
self.query_one("#user-input", Input).focus()
|
|
157
164
|
|
|
@@ -215,8 +222,73 @@ class TermAgent(App):
|
|
|
215
222
|
self._start_spinner()
|
|
216
223
|
self.process_input(user_input)
|
|
217
224
|
|
|
225
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
226
|
+
if self._confirmation_handler:
|
|
227
|
+
self._confirmation_handler(event)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
user_input = event.value.strip()
|
|
231
|
+
if not user_input:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
input_widget = self.query_one("#user-input", Input)
|
|
235
|
+
input_widget.clear()
|
|
236
|
+
|
|
237
|
+
if user_input.lower() == "bye":
|
|
238
|
+
self.exit()
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
log = self.query_one("#output-log", RichLog)
|
|
242
|
+
|
|
243
|
+
# ── NEW: raw command mode ──────────────────────────────
|
|
244
|
+
if user_input.startswith("!"):
|
|
245
|
+
raw_cmd = user_input[1:].strip()
|
|
246
|
+
if raw_cmd:
|
|
247
|
+
log.write(Text.from_markup(
|
|
248
|
+
f"\n[bold magenta]![/bold magenta] [white]{raw_cmd}[/white]"
|
|
249
|
+
f" [dim magenta](raw)[/dim magenta]"
|
|
250
|
+
))
|
|
251
|
+
self._start_spinner()
|
|
252
|
+
self.run_raw_command(raw_cmd) # new worker, see Step 4
|
|
253
|
+
return
|
|
254
|
+
# ──────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
log.write(Text.from_markup(f"\n[bold cyan]❯[/bold cyan] [white]{user_input}[/white]"))
|
|
257
|
+
self._start_spinner()
|
|
258
|
+
self.process_input(user_input)
|
|
259
|
+
|
|
218
260
|
# ── Agent worker ─────────────────────────────────────────────────────────
|
|
261
|
+
@work(thread=True)
|
|
262
|
+
def run_raw_command(self, cmd: str) -> None:
|
|
263
|
+
"""Execute a PowerShell command directly, bypassing the agent."""
|
|
264
|
+
import subprocess
|
|
265
|
+
from rich.markup import escape
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
result = subprocess.run(
|
|
269
|
+
["powershell", "-Command",
|
|
270
|
+
f"{cmd}; Get-Location | Select-Object -ExpandProperty Path"],
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
cwd=self.cwd
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if result.returncode == 0:
|
|
277
|
+
lines = result.stdout.strip().splitlines()
|
|
278
|
+
new_cwd = lines[-1].strip() if lines else self.cwd
|
|
279
|
+
output = "\n".join(lines[:-1]) if len(lines) > 1 else "Command executed successfully."
|
|
280
|
+
self.call_from_thread(self._update_output, output, "command", new_cwd)
|
|
281
|
+
else:
|
|
282
|
+
error = result.stderr.strip() or "Unknown error"
|
|
283
|
+
self.call_from_thread(self._update_output, f"Error: {error}", "command", self.cwd)
|
|
219
284
|
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.call_from_thread(self._stop_spinner)
|
|
287
|
+
self.call_from_thread(
|
|
288
|
+
self._set_status,
|
|
289
|
+
f"[bold red]✗ Error: {escape(str(e))}[/bold red]"
|
|
290
|
+
)
|
|
291
|
+
|
|
220
292
|
@work(thread=True)
|
|
221
293
|
def process_input(self, user_input: str) -> None:
|
|
222
294
|
from termagent.agent.graph import app as agent_app
|
|
@@ -240,7 +312,8 @@ class TermAgent(App):
|
|
|
240
312
|
nodes._confirm_fn = patched_confirm
|
|
241
313
|
|
|
242
314
|
try:
|
|
243
|
-
|
|
315
|
+
email_enabled = bool(os.getenv("EMAIL_ADDRESS") and os.getenv("EMAIL_PASSWORD"))
|
|
316
|
+
state = {"text": user_input, "cwd": self.cwd, "user_name": os.getenv("EMAIL_USERNAME"), "email_enabled": email_enabled}
|
|
244
317
|
result = agent_app.invoke(state)
|
|
245
318
|
|
|
246
319
|
new_cwd = result.get("cwd", self.cwd)
|
|
@@ -262,7 +335,7 @@ class TermAgent(App):
|
|
|
262
335
|
def _ask_confirmation(self, cmd: str, result_holder: dict, event) -> None:
|
|
263
336
|
# Stop spinner while waiting for user
|
|
264
337
|
self._stop_spinner()
|
|
265
|
-
self._set_status("[bold yellow]
|
|
338
|
+
self._set_status("[bold yellow] Risky command — type yes or no[/bold yellow]")
|
|
266
339
|
|
|
267
340
|
log = self.query_one("#output-log", RichLog)
|
|
268
341
|
log.write(Text.from_markup(
|
|
@@ -298,6 +371,18 @@ class TermAgent(App):
|
|
|
298
371
|
self._stop_spinner()
|
|
299
372
|
log = self.query_one("#output-log", RichLog)
|
|
300
373
|
|
|
374
|
+
if output == "EMAIL_SETUP_REQUIRED":
|
|
375
|
+
log.write(Text.from_markup(
|
|
376
|
+
"[bold yellow] Email credentials not set up.[/bold yellow]\n"
|
|
377
|
+
" [dim]Please set [bold cyan]EMAIL_ADDRESS[/bold cyan] and [bold cyan]EMAIL_PASSWORD[/bold cyan] in your [bold].env[/bold] file.\n"
|
|
378
|
+
" Gmail users: generate an App Password at [bold cyan]myaccount.google.com/apppasswords[/bold cyan][/dim]\n"
|
|
379
|
+
" [dim]--------------------------OR--------------------------[/dim]\n"
|
|
380
|
+
" [dim]Restart TERMAGENT and choose yes to enter credentials interactively.[/dim]"
|
|
381
|
+
))
|
|
382
|
+
self._set_status("[bold yellow] Email setup required[/bold yellow]")
|
|
383
|
+
self.cwd = new_cwd
|
|
384
|
+
return
|
|
385
|
+
|
|
301
386
|
if intent == "chat":
|
|
302
387
|
self._set_status("[dim cyan]◌ responded[/dim cyan]")
|
|
303
388
|
log.write(Text.from_markup(f" [white]{escape(output)}[/white]"))
|
|
@@ -325,32 +410,6 @@ def main():
|
|
|
325
410
|
load_dotenv()
|
|
326
411
|
|
|
327
412
|
groq_key = os.getenv("GROQ_API_KEY")
|
|
328
|
-
email_user = os.getenv("EMAIL_ADDRESS")
|
|
329
|
-
email_pass = os.getenv("EMAIL_PASSWORD")
|
|
330
|
-
|
|
331
|
-
if not email_user:
|
|
332
|
-
print("Email credentials not found.")
|
|
333
|
-
print("Email credentials will only be used when sending an email.")
|
|
334
|
-
print("""
|
|
335
|
-
Google doesn't allow regular passwords for SMTP. They need to generate an App Password:
|
|
336
|
-
|
|
337
|
-
Go to Google Account → Security → 2-Step Verification → App Passwords
|
|
338
|
-
Generate one for "Mail"
|
|
339
|
-
""")
|
|
340
|
-
email_user_name = input("Enter your name(used for email signatures): ")
|
|
341
|
-
email_user = input("Enter your email address: ").strip()
|
|
342
|
-
email_pass = input("Enter your email password/app password: ").strip()
|
|
343
|
-
|
|
344
|
-
save = input("Save to .env for future use? (yes/no): ")
|
|
345
|
-
if save.lower() == "yes":
|
|
346
|
-
with open(".env", "a") as f:
|
|
347
|
-
f.write(f"\nEMAIL_ADDRESS={email_user}")
|
|
348
|
-
f.write(f"\nEMAIL_PASSWORD={email_pass}")
|
|
349
|
-
f.write(f"\nEMAIL_USERNAME={email_user_name}")
|
|
350
|
-
|
|
351
|
-
os.environ["EMAIL_ADDRESS"] = email_user
|
|
352
|
-
os.environ["EMAIL_PASSWORD"] = email_pass
|
|
353
|
-
os.environ["EMAIL_USERNAME"] = email_user_name
|
|
354
413
|
|
|
355
414
|
if not groq_key:
|
|
356
415
|
print("Groq API key not found.")
|
|
@@ -364,6 +423,38 @@ def main():
|
|
|
364
423
|
|
|
365
424
|
os.environ["GROQ_API_KEY"] = groq_key
|
|
366
425
|
|
|
426
|
+
email_user = os.getenv("EMAIL_ADDRESS")
|
|
427
|
+
email_pass = os.getenv("EMAIL_PASSWORD")
|
|
428
|
+
|
|
429
|
+
if email_user and email_pass:
|
|
430
|
+
print("Email features enabled.")
|
|
431
|
+
else:
|
|
432
|
+
email_enabled = input("Enable email features? (yes/no): ").strip().lower()
|
|
433
|
+
if email_enabled == "yes":
|
|
434
|
+
print("Email credentials not found.")
|
|
435
|
+
print("Email credentials will only be used when sending an email.")
|
|
436
|
+
print("""
|
|
437
|
+
Google doesn't allow regular passwords for SMTP.
|
|
438
|
+
Go to myaccount.google.com/apppasswords and generate an App Password for "Mail".
|
|
439
|
+
""")
|
|
440
|
+
email_user_name = input("Enter your name (used for email signatures): ")
|
|
441
|
+
email_user = input("Enter your email address: ").strip()
|
|
442
|
+
email_pass = input("Enter your email password/app password: ").strip()
|
|
443
|
+
|
|
444
|
+
save = input("Save to .env for future use? (yes/no): ")
|
|
445
|
+
if save.lower() == "yes":
|
|
446
|
+
with open(".env", "a") as f:
|
|
447
|
+
f.write(f"\nEMAIL_ADDRESS={email_user}")
|
|
448
|
+
f.write(f"\nEMAIL_PASSWORD={email_pass}")
|
|
449
|
+
f.write(f"\nEMAIL_USERNAME={email_user_name}")
|
|
450
|
+
|
|
451
|
+
os.environ["EMAIL_ADDRESS"] = email_user
|
|
452
|
+
os.environ["EMAIL_PASSWORD"] = email_pass
|
|
453
|
+
os.environ["EMAIL_USERNAME"] = email_user_name
|
|
454
|
+
else:
|
|
455
|
+
print("Email features disabled. Set EMAIL_ADDRESS and EMAIL_PASSWORD in .env to enable later.")
|
|
456
|
+
|
|
457
|
+
print("Starting TERMAGENT...")
|
|
367
458
|
app = TermAgent()
|
|
368
459
|
app.run()
|
|
369
460
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: termagent-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Natural language terminal agent for Windows PowerShell
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: langchain-groq
|
|
8
|
+
Requires-Dist: langchain-core
|
|
9
|
+
Requires-Dist: langchain
|
|
10
|
+
Requires-Dist: langgraph
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: python-dotenv
|
|
13
|
+
Requires-Dist: ollama
|
|
14
|
+
Requires-Dist: textual
|
|
15
|
+
Requires-Dist: rich
|
|
16
|
+
|
|
17
|
+
# Termagent: Natural Language PowerShell Agent
|
|
18
|
+
|
|
19
|
+
Termagent is a powerful, Windows-native AI agent that transforms natural language instructions into executable PowerShell commands. Built for both non-technical users and power developers, it bridges the gap between intent and execution through a sophisticated agentic workflow.
|
|
20
|
+
|
|
21
|
+
## 🚀 Key Features
|
|
22
|
+
|
|
23
|
+
- **Natural Language Translation**: Convert plain English into complex PowerShell scripts instantly.
|
|
24
|
+
- **Stateful Orchestration**: Built on **LangGraph**, allowing the agent to remember context, navigate directories, and handle multi-step operations.
|
|
25
|
+
- **Dual-Layer Safety Architecture**:
|
|
26
|
+
- **Static Blacklist**: Prevents system-critical operations (e.g., System32 access, Registry edits) before they reach the LLM.
|
|
27
|
+
- **LLM Security Review**: A second layer of reasoning that flags context-specific risks.
|
|
28
|
+
- **Human-in-the-Loop (HITL)**: Risky commands are never executed without explicit user confirmation.
|
|
29
|
+
- **Textual TUI**: A beautiful, responsive terminal user interface for a premium CLI experience.
|
|
30
|
+
- **Email Integration**: Compose and send professional emails with attachments directly from the terminal.
|
|
31
|
+
|
|
32
|
+
## 🛠️ Architecture
|
|
33
|
+
|
|
34
|
+
Termagent utilizes a Directed Acyclic Graph (DAG) state machine to manage the conversation flow, tool selection, and command validation. By running inference on **Groq**, it ensures near-instant response times while maintaining high reasoning quality.
|
|
35
|
+
|
|
36
|
+
## ⚠️ Safety First
|
|
37
|
+
|
|
38
|
+
While Termagent is designed to be helpful, it executes real commands on your system. Always review flagged commands before confirming execution.
|
termagent_cli-0.2.0/PKG-INFO
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: termagent-cli
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Natural language terminal agent for Windows PowerShell
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: langchain-groq
|
|
7
|
-
Requires-Dist: langchain-core
|
|
8
|
-
Requires-Dist: langchain
|
|
9
|
-
Requires-Dist: langgraph
|
|
10
|
-
Requires-Dist: pydantic
|
|
11
|
-
Requires-Dist: python-dotenv
|
|
12
|
-
Requires-Dist: ollama
|
|
13
|
-
Requires-Dist: textual
|
|
14
|
-
Requires-Dist: rich
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: termagent-cli
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Natural language terminal agent for Windows PowerShell
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: langchain-groq
|
|
7
|
-
Requires-Dist: langchain-core
|
|
8
|
-
Requires-Dist: langchain
|
|
9
|
-
Requires-Dist: langgraph
|
|
10
|
-
Requires-Dist: pydantic
|
|
11
|
-
Requires-Dist: python-dotenv
|
|
12
|
-
Requires-Dist: ollama
|
|
13
|
-
Requires-Dist: textual
|
|
14
|
-
Requires-Dist: rich
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|