askme-ai-cli 0.1.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,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: askme-ai-cli
3
+ Version: 0.1.0
4
+ Summary: A professional iterative CLI AI Agent powered by Ollama
5
+ Author-email: Tharun Kumar <buddetharunkumar123@example.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Interpreters
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Environment :: Console
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: ollama>=0.6.2
16
+ Requires-Dist: rich>=13.7.0
17
+ Requires-Dist: requests>=2.31.0
File without changes
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import os
4
+ import platform
5
+ import sys
6
+ import requests
7
+ import json
8
+ import re
9
+ import subprocess
10
+ import tempfile
11
+ import shlex
12
+ from datetime import datetime
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.prompt import Confirm, Prompt
16
+ from rich.live import Live
17
+ from rich.text import Text
18
+
19
+ console = Console()
20
+
21
+ def apply_patch(filepath, diff_content):
22
+ """Applies a unified diff to a file using the system patch utility."""
23
+ try:
24
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.diff', delete=False) as tmp:
25
+ tmp.write(diff_content)
26
+ tmp_path = tmp.name
27
+
28
+ # -u treats it as unified diff, -N allows new files
29
+ result = subprocess.run(['patch', '-u', filepath, tmp_path], capture_output=True, text=True)
30
+ os.unlink(tmp_path)
31
+
32
+ if result.returncode == 0:
33
+ return f"Successfully applied patch to {filepath}."
34
+ else:
35
+ return f"Patch failed:\n{result.stderr}"
36
+ except Exception as e:
37
+ return f"Error applying patch: {e}"
38
+
39
+ def summarize_history(history, api_key, model_name):
40
+ """Calls the LLM to summarize interaction history into a concise project state."""
41
+ url = "https://ollama.com/api/generate"
42
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
43
+ history_text = "\n".join([f"User: {h['q']}\nAgent: {h['a']}" for h in history])
44
+ summary_prompt = f"Summarize this interaction history into a single concise paragraph describing the current 'State of the Project'. Mention key accomplishments and current file states.\n\n[HISTORY]\n{history_text}"
45
+
46
+ payload = {"model": model_name, "prompt": summary_prompt, "stream": False}
47
+ try:
48
+ response = requests.post(url, headers=headers, json=payload, timeout=30)
49
+ return response.json().get("response", "Summary unavailable.")
50
+ except Exception:
51
+ return "Summary generation failed."
52
+
53
+ def log_execution(command, returncode, duration, session="default"):
54
+ """Records execution details to a session log file."""
55
+ log_dir = ".askme/logs"
56
+ os.makedirs(log_dir, exist_ok=True)
57
+ log_path = os.path.join(log_dir, f"{session}.log")
58
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
59
+ with open(log_path, "a") as f:
60
+ f.write(f"[{timestamp}] CMD: {command} | EXIT: {returncode} | DUR: {duration:.2f}s\n")
61
+
62
+ def load_history(history_file):
63
+ """Loads conversation history from the specified file."""
64
+ try:
65
+ with open(history_file, "r") as f:
66
+ return json.load(f)
67
+ except (FileNotFoundError, json.JSONDecodeError):
68
+ return []
69
+ except Exception as e:
70
+ console.print(f"[yellow]Warning: Could not load history: {e}[/yellow]")
71
+ return []
72
+
73
+ def save_history(history, history_file):
74
+ """Saves the last 10 interactions to maintain project context."""
75
+ with open(history_file, "w") as f:
76
+ json.dump(history[-10:], f)
77
+
78
+ def get_directory_tree(path=".", prefix="", depth=0, max_depth=2):
79
+ """Generates a visual directory tree string."""
80
+ if depth > max_depth:
81
+ return []
82
+ tree = []
83
+ try:
84
+ items = sorted(os.listdir(path))
85
+ # Filter out common junk and hidden files except history
86
+ items = [i for i in items if not i.startswith('.') or i == '.askme']
87
+ for i, item in enumerate(items):
88
+ connector = "└── " if i == len(items) - 1 else "├── "
89
+ tree.append(f"{prefix}{connector}{item}")
90
+ full_path = os.path.join(path, item)
91
+ if os.path.isdir(full_path):
92
+ extension = " " if i == len(items) - 1 else "│ "
93
+ tree.extend(get_directory_tree(full_path, prefix + extension, depth + 1))
94
+ except Exception: pass
95
+ return tree
96
+
97
+ def get_system_context():
98
+ """Gathers environment info to improve agent awareness."""
99
+ return {
100
+ "os": platform.system(),
101
+ "os_version": platform.release(),
102
+ "python_version": sys.version.split()[0],
103
+ "cwd": os.getcwd()
104
+ }
105
+
106
+ def is_dangerous(command):
107
+ """Checks if a command contains potentially destructive operations using regex."""
108
+ dangerous_patterns = [
109
+ r"rm\s+-rf\s+/", r"rm\s+-rf\s+\*", r"rm\s+-rf\s+\.",
110
+ r"> /dev/sd", r"mkfs", r"chmod\s+-R\s+777",
111
+ r"dd\s+if=", r":\(\)\{\s+:\|:&\s+\};:",
112
+ r"mv\s+.*\s+/dev/null", r"shutdown", r"reboot"
113
+ ]
114
+ return any(re.search(pattern, command) for pattern in dangerous_patterns)
115
+
116
+ def is_filesystem_modifying(command):
117
+ """Checks if a command is likely to modify the directory structure."""
118
+ modifiers = ["mkdir", "touch", "rm", "mv", "cp", "git", ">>", ">"]
119
+ return any(mod in command for mod in modifiers)
120
+
121
+ def is_path_safe(command):
122
+ """Prevents directory traversal and access to absolute system paths."""
123
+ # Basic jail: block climbing up or absolute paths that don't start with CWD
124
+ return ".." not in command and not (command.startswith("/") and not command.startswith(os.getcwd()))
125
+
126
+ def get_current_context():
127
+ """Refreshes the directory tree and system info for the prompt."""
128
+ try:
129
+ tree = get_directory_tree()
130
+ sys_ctx = get_system_context()
131
+ return (
132
+ f"Environment: {sys_ctx['os']} {sys_ctx['os_version']}, Python {sys_ctx['python_version']}\n"
133
+ f"CWD: {sys_ctx['cwd']}\nProject Structure:\n" + "\n".join(tree)
134
+ )
135
+ except Exception:
136
+ return "Unknown directory context."
137
+
138
+ def main():
139
+ # 1. Setup argument parsing
140
+ parser = argparse.ArgumentParser(description="askme - The Professional CLI AI Agent")
141
+ parser.add_argument("-p", "--prompt", type=str, nargs='+', help="The prompt to send to the model")
142
+ parser.add_argument("-s", "--session", type=str, default="default", help="Session name for the history")
143
+ parser.add_argument("-m", "--model", type=str, default="gemma4:31b-cloud", help="Ollama model to use")
144
+ parser.add_argument("-d", "--dry-run", action="store_true", help="Show proposed commands without executing them")
145
+ args = parser.parse_args()
146
+
147
+ # 2. Configuration for Ollama
148
+ api_key = os.getenv("OLLAMA_API_KEY")
149
+ url = "https://ollama.com/api/generate"
150
+
151
+ console.print("[bold blue]Verifying Environment...[/bold blue]")
152
+
153
+ if not api_key:
154
+ console.print("[bold yellow]OLLAMA_API_KEY not found in environment.[/bold yellow]")
155
+ api_key = Prompt.ask("[bold cyan]Please enter your Ollama API Key[/bold cyan]", password=True)
156
+ if not api_key:
157
+ console.print("[bold red]Error: API Key is required to proceed.[/bold red]")
158
+ return
159
+
160
+ # Prompt for model selection if key was missing
161
+ console.print("\n[bold white]Select an AI Model to use:[/bold white]")
162
+ console.print("1. gemma4:31b-cloud (Default)")
163
+ console.print("2. llama3")
164
+ console.print("3. mistral")
165
+ choice = Prompt.ask("Choose a number or type a custom model name", default="1")
166
+
167
+ if choice == "1": args.model = "gemma4:31b-cloud"
168
+ elif choice == "2": args.model = "llama3"
169
+ elif choice == "3": args.model = "mistral"
170
+ else: args.model = choice
171
+
172
+ try:
173
+ requests.head(url.rsplit('/', 1)[0], timeout=5)
174
+ console.print(f"[green]✔[/green] OLLAMA_API_KEY detected and endpoint reachable.")
175
+ console.print(f"[green]✔[/green] Using model: [bold cyan]{args.model}[/bold cyan]\n")
176
+ except Exception as e:
177
+ console.print(f"[bold yellow]![/bold yellow] Warning: Connectivity check to {url} failed: {e}\n")
178
+
179
+ # 2.1 Define Agent behavior for iterative reasoning
180
+ system_instruction = (
181
+ "You are an iterative CLI Agent. Solve user requests by planning and executing shell commands.\n"
182
+ "1. Analyze context and provide reasoning.\n"
183
+ "2. For standard execution, use <execute>command</execute> (shlex-safe).\n"
184
+ "3. For complex commands with pipes or redirects, use <execute_shell>command</execute_shell>.\n"
185
+ "4. For large files, prefer using 'grep', 'head', or 'tail' to read specific sections.\n"
186
+ "5. Use command output to decide your next step.\n"
187
+ "6. Use <patch file=\"path\">unified diff content</patch> to modify files precisely without overwriting.\n"
188
+ "7. When finished, provide a final response."
189
+ )
190
+
191
+ # 2.2 Setup Session and Load History
192
+ session_dir = ".askme/sessions"
193
+ os.makedirs(session_dir, exist_ok=True)
194
+ history_path = os.path.join(session_dir, f"{args.session}.json")
195
+ history = load_history(history_path)
196
+
197
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
198
+ session_vars = {} # Maintain environment variables across interactive turns
199
+
200
+ # 3. Determine if we are in one-shot or interactive mode
201
+ if args.prompt:
202
+ user_prompt = " ".join(args.prompt) if isinstance(args.prompt, list) else args.prompt
203
+ run_agent_loop(user_prompt, system_instruction, url, headers, args, history, history_path, api_key, session_vars, args.model)
204
+ else:
205
+ console.print(Panel("[bold cyan]Entering Interactive Mode[/bold cyan]\nType [bold red]'exit'[/bold red] or [bold red]'quit'[/bold red] to stop.", border_style="blue"))
206
+ while True:
207
+ user_prompt = Prompt.ask("[bold yellow]Query[/bold yellow]")
208
+ if user_prompt.lower() in ["exit", "quit"]:
209
+ break
210
+
211
+ run_agent_loop(user_prompt, system_instruction, url, headers, args, history, history_path, api_key, session_vars, args.model)
212
+
213
+ def run_agent_loop(user_prompt, system_instruction, url, headers, args, history, history_path, api_key, session_vars, model_name):
214
+ # Refresh context and summary at the start of every loop turn
215
+ project_context = get_current_context()
216
+ summary_block = ""
217
+ if len(history) >= 10:
218
+ console.print("[dim]Session history is long. Generating semantic summary to save tokens...[/dim]")
219
+ summary = summarize_history(history[:-3], api_key, model_name)
220
+ summary_block = f"[PROJECT SUMMARY]\n{summary}\n\n"
221
+ # Update history reference to keep the most recent 3 for immediate context
222
+ history[:] = history[-3:]
223
+
224
+ history_str = "\n".join([f"User: {h['q']}\nAgent: {h['a']}" for h in history])
225
+ current_prompt = f"{system_instruction}\n\n[PROJECT CONTEXT]\n{project_context}\n\n{summary_block}[RECENT HISTORY]\n{history_str}\n\nUser: {user_prompt}"
226
+ all_agent_responses = []
227
+ iteration = 0
228
+ max_iterations = 5
229
+ tree_dirty = False
230
+
231
+ total_prompt_tokens = 0
232
+ total_completion_tokens = 0
233
+ start_session_time = datetime.now()
234
+
235
+ try:
236
+ while iteration < max_iterations:
237
+ iteration += 1
238
+ payload = {
239
+ "model": model_name,
240
+ "prompt": current_prompt,
241
+ "stream": True,
242
+ "options": {
243
+ "num_ctx": 8192,
244
+ "temperature": 0.2 # Lower temperature for more precise CLI commands
245
+ }
246
+ }
247
+ response = requests.post(url, headers=headers, json=payload, stream=True)
248
+ response.raise_for_status()
249
+
250
+ console.print(f"\n[bold cyan] askme Agent[/bold cyan]: ", end="")
251
+ full_content = ""
252
+ for line in response.iter_lines():
253
+ if line:
254
+ chunk = json.loads(line.decode("utf-8"))
255
+ if "response" in chunk:
256
+ token = chunk["response"]
257
+ console.print(token, end="", style="italic green")
258
+ full_content += token
259
+ if chunk.get("done"):
260
+ total_prompt_tokens += chunk.get("prompt_eval_count", 0)
261
+ total_completion_tokens += chunk.get("eval_count", 0)
262
+ print()
263
+ all_agent_responses.append(full_content)
264
+
265
+ # Parse for the command to execute
266
+ # Uses regex to find commands even inside code blocks or with trailing spaces
267
+ command = None
268
+ use_shell = False
269
+
270
+ shell_match = re.search(r"<execute_shell>(.*?)</execute_shell>", full_content, re.DOTALL)
271
+ exec_match = re.search(r"<execute>(.*?)</execute>", full_content, re.DOTALL)
272
+ patch_match = re.search(r"<patch file=['\"](.*?)['\"]>(.*?)</patch>", full_content, re.DOTALL)
273
+
274
+ if shell_match:
275
+ command = shell_match.group(1).strip()
276
+ use_shell = True
277
+ elif patch_match:
278
+ filepath, diff_content = patch_match.groups()
279
+ res = apply_patch(filepath, diff_content)
280
+ current_prompt += f"\nAgent: {full_content}\n[SYSTEM: Patch Result]\n{res}\nNext step:"
281
+ tree_dirty = True
282
+ continue
283
+ elif exec_match:
284
+ command = exec_match.group(1).strip()
285
+ use_shell = False
286
+
287
+ if not command:
288
+ break
289
+
290
+ # The Shield: Safety check for dangerous commands
291
+ if is_dangerous(command) or not is_path_safe(command):
292
+ console.print(Panel(
293
+ f"[bold red]SECURITY ALERT:[/bold red] Unsafe command or path detected!\n"
294
+ f"Command: [bold cyan]{command}[/bold cyan]\n\n"
295
+ "Execution blocked for safety.",
296
+ title="[blink red]SECURITY BLOCK[/blink red]",
297
+ border_style="bold red"
298
+ ))
299
+ if not Confirm.ask("[bold red]Are you absolutely sure you want to run this?[/bold red]", default=False):
300
+ console.print("[bold yellow]! High-risk execution blocked by user.[/bold yellow]")
301
+ break
302
+
303
+ if args.dry_run:
304
+ console.print(Panel(f"DRY RUN: Would execute [bold cyan]{command}[/bold cyan]", border_style="yellow"))
305
+ break
306
+
307
+ mode_str = " (SHELL MODE)" if use_shell else ""
308
+ if Confirm.ask(f"\n[bold yellow]󰆍 Agent suggests{mode_str}:[/bold yellow] [bold blue]{command}[/bold blue]\n[dim]Execute?[/dim]"):
309
+ console.print(Panel(f"Executing: [bold white]{command}[/bold white]", border_style="blue", title="System" if not use_shell else "System (Shell)"))
310
+
311
+ output_text = ""
312
+ try:
313
+ if use_shell:
314
+ cmd_args = command
315
+ else:
316
+ cmd_args = shlex.split(command)
317
+
318
+ start_time = datetime.now()
319
+ # Merge system env with session variables
320
+ env = os.environ.copy()
321
+ env.update(session_vars)
322
+
323
+ process = subprocess.Popen(
324
+ cmd_args, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
325
+ text=True, bufsize=1, env=env
326
+ )
327
+
328
+ console.print("[dim]Command Output:[/dim]")
329
+ with Live(Text(""), refresh_per_second=4) as live:
330
+ for line in process.stdout:
331
+ output_text += line
332
+ display_text = "\n".join(output_text.splitlines()[-10:])
333
+ live.update(Text(display_text)) # Show last 10 lines
334
+
335
+ process.wait(timeout=30) # Prevent hanging on interactive commands
336
+ duration = (datetime.now() - start_time).total_seconds()
337
+
338
+ log_execution(command, process.returncode, duration, args.session)
339
+
340
+ if process.returncode == 0:
341
+ console.print("[bold green]✔ Success.[/bold green]")
342
+ system_status = "Command result"
343
+ else:
344
+ console.print(f"[bold red]✘ Failed with code {process.returncode}[/bold red]")
345
+ system_status = f"Command failed with exit code {process.returncode}. Analyze the error below."
346
+
347
+ tree_dirty = is_filesystem_modifying(command)
348
+
349
+ except subprocess.TimeoutExpired:
350
+ process.kill()
351
+ console.print("[bold red]✘ Command timed out (30s).[/bold red]")
352
+ output_text += "\n[ERROR: Process terminated due to timeout]"
353
+ system_status = "Command timed out. Use a more targeted command."
354
+ except Exception as e:
355
+ console.print(f"[bold red]✘ Execution error:[/bold red] {e}")
356
+ output_text += f"\n[ERROR: {e}]"
357
+ system_status = "Execution error"
358
+
359
+ # Token Optimization: Truncate very long outputs in the prompt
360
+ if len(output_text) > 4000:
361
+ output_text = output_text[:2000] + "\n... [Output truncated for brevity] ...\n" + output_text[-2000:]
362
+
363
+ # Update context with output for the next reasoning step
364
+ current_prompt += f"\nAgent: {full_content}\n[SYSTEM: {system_status}]\n{output_text}"
365
+
366
+ if tree_dirty:
367
+ new_tree = get_directory_tree()
368
+ current_prompt += f"\n[UPDATED PROJECT STRUCTURE]\n" + "\n".join(new_tree)
369
+
370
+ current_prompt += "\nNext step:"
371
+ else:
372
+ console.print("[bold yellow]! Execution skipped by user.[/bold yellow]")
373
+ break
374
+
375
+ if iteration >= max_iterations:
376
+ console.print(Panel("[yellow]Maximum iteration limit reached for this request.[/yellow]", border_style="yellow"))
377
+
378
+ # Display Metadata Summary
379
+ end_session_time = datetime.now()
380
+ total_duration = (end_session_time - start_session_time).total_seconds()
381
+
382
+ summary_text = Text()
383
+ summary_text.append(f"Total Iterations: ", style="bold white")
384
+ summary_text.append(f"{iteration}\n", style="cyan")
385
+ summary_text.append(f"Prompt Tokens: ", style="bold white")
386
+ summary_text.append(f"{total_prompt_tokens} ", style="cyan")
387
+ summary_text.append(f"| Completion Tokens: ", style="bold white")
388
+ summary_text.append(f"{total_completion_tokens}\n", style="cyan")
389
+ summary_text.append(f"Total Time: ", style="bold white")
390
+ summary_text.append(f"{total_duration:.2f}s", style="cyan")
391
+
392
+ console.print(Panel(summary_text, title="[bold magenta]Session Metadata[/bold magenta]", border_style="magenta"))
393
+
394
+ # Save the cumulative interaction
395
+ history.append({"q": user_prompt, "a": "\n".join(all_agent_responses)})
396
+ save_history(history, history_path)
397
+
398
+ except KeyboardInterrupt:
399
+ console.print("\n[bold red]Operation cancelled by user.[/bold red]")
400
+ except Exception as e:
401
+ console.print(f"\n[bold red]Error occurred:[/bold red] {e}")
402
+
403
+ if __name__ == "__main__":
404
+ main()
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: askme-ai-cli
3
+ Version: 0.1.0
4
+ Summary: A professional iterative CLI AI Agent powered by Ollama
5
+ Author-email: Tharun Kumar <buddetharunkumar123@example.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Interpreters
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Environment :: Console
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: ollama>=0.6.2
16
+ Requires-Dist: rich>=13.7.0
17
+ Requires-Dist: requests>=2.31.0
@@ -0,0 +1,9 @@
1
+ README.md
2
+ askme.py
3
+ pyproject.toml
4
+ askme_ai_cli.egg-info/PKG-INFO
5
+ askme_ai_cli.egg-info/SOURCES.txt
6
+ askme_ai_cli.egg-info/dependency_links.txt
7
+ askme_ai_cli.egg-info/entry_points.txt
8
+ askme_ai_cli.egg-info/requires.txt
9
+ askme_ai_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ askme = askme:main
@@ -0,0 +1,3 @@
1
+ ollama>=0.6.2
2
+ rich>=13.7.0
3
+ requests>=2.31.0
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "askme-ai-cli"
3
+ version = "0.1.0"
4
+ description = "A professional iterative CLI AI Agent powered by Ollama"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ authors = [{ name = "Tharun Kumar", email = "buddetharunkumar123@example.com" }]
8
+ license = { text = "MIT" }
9
+ dependencies = [
10
+ "ollama>=0.6.2",
11
+ "rich>=13.7.0",
12
+ "requests>=2.31.0",
13
+ ]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Software Development :: Interpreters",
19
+ "Intended Audience :: Developers",
20
+ "Environment :: Console",
21
+ ]
22
+
23
+ [project.scripts]
24
+ askme = "askme:main"
25
+
26
+ [build-system]
27
+ requires = ["setuptools>=61.0"]
28
+ build-backend = "setuptools.build_meta"
29
+
30
+ [tool.setuptools]
31
+ py-modules = ["askme"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+