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.
@@ -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
- ![demo-image](assets/Screenshot.png)
19
+ ![demo-image](assets/demo.png)
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
+ ![demo-image](assets/graph.png)
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](https://support.google.com/accounts/answer/185833?hl=en) (prompted on first run) |
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.2.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 "email_node"
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
- "email_node": "email_node"
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
- email: Optional[EmailOutput] = Field(None, description="The email to send, if intent is 'email'")
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
- - If the intent is "email", populate the email field with recipient, subject, body, and attachment (if any).
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
- # llm = ChatOllama(model=OLLAMA_MODEL)
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
- attachment_path = os.path.join(state['cwd'], email_data['attachment'])
230
- with open(attachment_path, 'rb') as f:
231
- part = MIMEBase('application', 'octet-stream')
232
- part.set_payload(f.read())
233
- encoders.encode_base64(part)
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.SMTP_SSL('smtp.gmail.com', 465) as server:
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}
@@ -10,4 +10,6 @@ class AgentState(TypedDict):
10
10
  response: Optional[str]
11
11
  result: Optional[str]
12
12
  email: Optional[dict]
13
- user_name: Optional[str]
13
+ user_name: Optional[str]
14
+ email_enabled: Optional[bool]
15
+ early_exit: Optional[bool]
@@ -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(placeholder="Ask me anything or describe what to do...", id="user-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. Type [bold cyan]bye[/bold cyan] to exit.[/dim]\n"
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
- state = {"text": user_input, "cwd": self.cwd, "user_name": os.getenv("EMAIL_USERNAME")}
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] Risky command — type yes or no[/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,4 @@
1
+ from agent.graph import app
2
+
3
+ with open("graph.png", "wb") as f:
4
+ f.write(app.get_graph().draw_mermaid_png())
@@ -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.
@@ -1,7 +1,9 @@
1
1
  README.md
2
+ package_info.md
2
3
  pyproject.toml
3
4
  termagent/__init__.py
4
5
  termagent/ui.py
6
+ termagent/viz.py
5
7
  termagent/agent/graph.py
6
8
  termagent/agent/nodes.py
7
9
  termagent/agent/state.py
@@ -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