notrack-cli-agent 0.6.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.

Potentially problematic release.


This version of notrack-cli-agent might be problematic. Click here for more details.

notrack_cli/memory.py ADDED
@@ -0,0 +1,73 @@
1
+ import os
2
+ import json
3
+ import time
4
+
5
+ HISTORY_DIR = os.path.expanduser("~/.notrack_cli")
6
+ SESSION_FILE = os.path.join(HISTORY_DIR, "session_history.json")
7
+ CHECKPOINT_FILE = os.path.join(HISTORY_DIR, "checkpoint.json")
8
+
9
+ class MemoryManager:
10
+ @staticmethod
11
+ def ensure_dir():
12
+ os.makedirs(HISTORY_DIR, exist_ok=True)
13
+
14
+ @staticmethod
15
+ def load_history():
16
+ MemoryManager.ensure_dir()
17
+ if os.path.exists(SESSION_FILE):
18
+ try:
19
+ with open(SESSION_FILE, "r", encoding="utf-8") as f:
20
+ return json.load(f)
21
+ except Exception:
22
+ return []
23
+ return []
24
+
25
+ @staticmethod
26
+ def save_history(messages):
27
+ MemoryManager.ensure_dir()
28
+ try:
29
+ with open(SESSION_FILE, "w", encoding="utf-8") as f:
30
+ json.dump(messages[-50:], f, ensure_ascii=False, indent=2)
31
+ except Exception:
32
+ pass
33
+
34
+ @staticmethod
35
+ def save_checkpoint(user_input, response_so_far, current_chat_id):
36
+ MemoryManager.ensure_dir()
37
+ data = {
38
+ "timestamp": time.time(),
39
+ "chat_id": current_chat_id,
40
+ "user_input": user_input,
41
+ "partial_response": response_so_far
42
+ }
43
+ try:
44
+ with open(CHECKPOINT_FILE, "w", encoding="utf-8") as f:
45
+ json.dump(data, f, ensure_ascii=False, indent=2)
46
+ except Exception:
47
+ pass
48
+
49
+ @staticmethod
50
+ def load_checkpoint():
51
+ if os.path.exists(CHECKPOINT_FILE):
52
+ try:
53
+ with open(CHECKPOINT_FILE, "r", encoding="utf-8") as f:
54
+ return json.load(f)
55
+ except Exception:
56
+ return None
57
+ return None
58
+
59
+ @staticmethod
60
+ def clear_checkpoint():
61
+ if os.path.exists(CHECKPOINT_FILE):
62
+ try:
63
+ os.remove(CHECKPOINT_FILE)
64
+ except Exception:
65
+ pass
66
+
67
+ @staticmethod
68
+ def clear_history():
69
+ if os.path.exists(SESSION_FILE):
70
+ try:
71
+ os.remove(SESSION_FILE)
72
+ except Exception:
73
+ pass
notrack_cli/parser.py ADDED
@@ -0,0 +1,36 @@
1
+ """Parse only explicit structured tool calls emitted by the model.
2
+
3
+ Plain prose or a fenced source-code sample is never interpreted as permission
4
+ for local execution.
5
+ """
6
+ import json
7
+ import re
8
+
9
+ _TAG_RE = re.compile(
10
+ r'<tool_call:([a-zA-Z_]\w*)>\s*(.*?)\s*</tool_call:\1>',
11
+ re.DOTALL | re.IGNORECASE,
12
+ )
13
+ _FENCE_RE = re.compile(
14
+ r'```tool_call:([a-zA-Z_]\w*)\s*\n(.*?)\n```',
15
+ re.DOTALL | re.IGNORECASE,
16
+ )
17
+
18
+
19
+ def extract_tool_calls(text: str) -> list[tuple[str, dict, str]]:
20
+ """Return explicit (tool name, JSON arguments, raw tag) calls only."""
21
+ calls = []
22
+ for pattern in (_TAG_RE, _FENCE_RE):
23
+ for match in pattern.finditer(text):
24
+ try:
25
+ args = json.loads(match.group(2))
26
+ except (json.JSONDecodeError, TypeError):
27
+ continue
28
+ if isinstance(args, dict):
29
+ calls.append((match.group(1), args, match.group(0)))
30
+ # Prevent duplicate calls if content somehow matches both grammars.
31
+ seen, unique = set(), []
32
+ for call in calls:
33
+ if call[2] not in seen:
34
+ seen.add(call[2])
35
+ unique.append(call)
36
+ return unique
notrack_cli/tools.py ADDED
@@ -0,0 +1,233 @@
1
+ import os
2
+ import subprocess
3
+ import shutil
4
+ import urllib.request
5
+ import json
6
+ import platform
7
+
8
+ # ──────────────────────────────────────────────────────────────
9
+ # SYSTEM PROMPT (sent to NoTrack AI on first turn)
10
+ # ──────────────────────────────────────────────────────────────
11
+ SYSTEM_PROMPT = """You are NoTrack Agent — an elite autonomous AI engineer and pair-programmer, running directly inside the user's terminal (Termux, Linux, macOS).
12
+
13
+ ━━━ IDENTITY ━━━
14
+ You are a senior software architect + DevOps engineer + security researcher in one.
15
+ You produce COMPLETE, PRODUCTION-GRADE, WORKING code. Never write stubs or placeholders.
16
+ You are direct and technically precise. No filler phrases. No apologies.
17
+ When given a coding task: PLAN → ACT (use tools) → VERIFY (run tests/commands).
18
+
19
+ ━━━ CRITICAL RULE ━━━
20
+ You have real tools that execute on the user's system RIGHT NOW.
21
+ When the user asks you to "create a file", "write a script", "run a command":
22
+ → USE THE TOOL. Do NOT just show code in markdown. USE write_file or run_command.
23
+ → The CLI will execute your tool calls and show the results live.
24
+
25
+ ━━━ AVAILABLE TOOLS ━━━
26
+
27
+ 1. CREATE OR WRITE A FILE (creates parent dirs automatically):
28
+ <tool_call:write_file>
29
+ {"path": "relative/or/absolute/path.ext", "content": "complete file content here"}
30
+ </tool_call:write_file>
31
+
32
+ 2. READ A FILE:
33
+ <tool_call:view_file>
34
+ {"path": "path/to/file"}
35
+ </tool_call:view_file>
36
+
37
+ 3. RUN ANY SHELL COMMAND (git, pip, npm, python, bash, rm, mkdir, zip, unzip — all allowed):
38
+ <tool_call:run_command>
39
+ {"command": "git push origin main"}
40
+ </tool_call:run_command>
41
+
42
+ 4. LIST DIRECTORY:
43
+ <tool_call:list_dir>
44
+ {"path": "."}
45
+ </tool_call:list_dir>
46
+
47
+ 5. SPAWN A PARALLEL SUB-AGENT for complex sub-tasks:
48
+ <tool_call:sub_agent>
49
+ {"task": "Detailed autonomous sub-task description"}
50
+ </tool_call:sub_agent>
51
+
52
+ 6. FETCH A WEB PAGE / DOCUMENTATION:
53
+ <tool_call:fetch_url>
54
+ {"url": "https://example.com/docs"}
55
+ </tool_call:fetch_url>
56
+
57
+ 7. SEARCH TEXT ACROSS FILES (Grep):
58
+ <tool_call:search_in_files>
59
+ {"query": "def my_function", "path": "."}
60
+ </tool_call:search_in_files>
61
+
62
+ 8. GET SYSTEM INFO:
63
+ <tool_call:get_system_info>
64
+ {}
65
+ </tool_call:get_system_info>
66
+
67
+ ━━━ BEHAVIORAL RULES ━━━
68
+ - Always use write_file to create files — never just show code in markdown.
69
+ - Always use run_command to execute things — never just say "run this".
70
+ - Chain tool calls: write → run → check output → fix if needed.
71
+ - For git: commit with meaningful messages, push after completing features.
72
+ - For Python: always check syntax with `python -m py_compile file.py` after writing.
73
+ - For projects: always create a proper directory structure, README, requirements.txt.
74
+ - If a command fails: read the error, fix it, run again. Keep going until it works.
75
+ - After finishing a multi-step task: summarize what was done.
76
+
77
+ ━━━ CODE QUALITY STANDARDS ━━━
78
+ - Complete code only — no "add your code here" comments.
79
+ - Proper error handling (try/except, error codes).
80
+ - Type hints for Python.
81
+ - Environment variables for secrets — never hardcode.
82
+ - Tests if the user asks for them.
83
+ - Production-ready: logging, edge cases, input validation.
84
+
85
+ ━━━ CONVERSATION STYLE ━━━
86
+ - Be concise and professional.
87
+ - When you need clarification: ask one focused question.
88
+ - When something is risky/destructive: say so clearly.
89
+ - Format your analysis/explanations with markdown headers.
90
+ - Show tool usage BEFORE the explanation (act first, explain after).
91
+ """
92
+
93
+
94
+ class LocalTools:
95
+
96
+ @staticmethod
97
+ def _resolve(path: str, cwd: str = None) -> str:
98
+ """Resolve path relative to cwd if not absolute, and expand ~."""
99
+ expanded = os.path.expanduser(path)
100
+ if cwd and not os.path.isabs(expanded):
101
+ return os.path.realpath(os.path.join(cwd, expanded))
102
+ return os.path.realpath(expanded)
103
+
104
+ @staticmethod
105
+ def view_file(path: str, cwd: str = None) -> str:
106
+ resolved = LocalTools._resolve(path, cwd)
107
+ try:
108
+ if not os.path.exists(resolved):
109
+ return f"Error: File not found: '{resolved}'"
110
+ with open(resolved, "r", encoding="utf-8", errors="ignore") as f:
111
+ content = f.read()
112
+ lines = content.splitlines()
113
+ if len(lines) > 1000:
114
+ content = "\n".join(lines[:1000]) + f"\n… [{len(lines)-1000} more lines]"
115
+ return content
116
+ except Exception as e:
117
+ return f"Error reading '{resolved}': {e}"
118
+
119
+ @staticmethod
120
+ def write_file(path: str, content: str, cwd: str = None) -> str:
121
+ resolved = LocalTools._resolve(path, cwd)
122
+ try:
123
+ os.makedirs(os.path.dirname(resolved), exist_ok=True)
124
+ with open(resolved, "w", encoding="utf-8") as f:
125
+ f.write(content)
126
+ return f"Written {len(content):,} bytes to '{resolved}'"
127
+ except Exception as e:
128
+ return f"Error writing '{resolved}': {e}"
129
+
130
+ @staticmethod
131
+ def run_command(command: str, cwd: str = None, timeout: int = 120) -> str:
132
+ """Execute any shell command with full privileges. No restrictions."""
133
+ work_dir = cwd or os.getcwd()
134
+ try:
135
+ result = subprocess.run(
136
+ command,
137
+ shell=True,
138
+ capture_output=True,
139
+ text=True,
140
+ timeout=timeout,
141
+ cwd=work_dir,
142
+ env={**os.environ, "TERM": "xterm-256color"},
143
+ )
144
+ parts = []
145
+ if result.stdout.strip():
146
+ parts.append(result.stdout.strip())
147
+ if result.stderr.strip():
148
+ parts.append(f"[STDERR]\n{result.stderr.strip()}")
149
+ if result.returncode != 0:
150
+ parts.append(f"[Exit {result.returncode}]")
151
+ return "\n\n".join(parts) if parts else "(command completed — no output)"
152
+ except subprocess.TimeoutExpired:
153
+ return f"Error: Command timed out after {timeout}s"
154
+ except Exception as e:
155
+ return f"Error: {e}"
156
+
157
+ @staticmethod
158
+ def list_dir(path: str = ".", cwd: str = None) -> str:
159
+ resolved = LocalTools._resolve(path, cwd)
160
+ try:
161
+ items = sorted(os.listdir(resolved))
162
+ lines = []
163
+ for item in items:
164
+ full = os.path.join(resolved, item)
165
+ if os.path.isdir(full):
166
+ try:
167
+ n = len(os.listdir(full))
168
+ lines.append(f"📁 {item}/ ({n} items)")
169
+ except PermissionError:
170
+ lines.append(f"📁 {item}/ (permission denied)")
171
+ else:
172
+ size = os.path.getsize(full)
173
+ lines.append(f"📄 {item} [{size:,} B]")
174
+ return "\n".join(lines) if lines else "(empty directory)"
175
+ except Exception as e:
176
+ return f"Error listing '{resolved}': {e}"
177
+
178
+ @staticmethod
179
+ def fetch_url(url: str) -> str:
180
+ """Download text content from a URL (documentation, APIs, etc.)."""
181
+ try:
182
+ req = urllib.request.Request(url, headers={'User-Agent': 'NoTrack-CLI'})
183
+ with urllib.request.urlopen(req, timeout=15) as response:
184
+ content = response.read().decode('utf-8', errors='ignore')
185
+ if len(content) > 15000:
186
+ content = content[:15000] + "\n… [Content truncated due to length]"
187
+ return content
188
+ except Exception as e:
189
+ return f"Error fetching URL '{url}': {e}"
190
+
191
+ @staticmethod
192
+ def search_in_files(query: str, path: str = ".", extension: str = None, cwd: str = None) -> str:
193
+ """Search for a text string inside files recursively."""
194
+ resolved = LocalTools._resolve(path, cwd)
195
+ results = []
196
+ try:
197
+ for root, dirs, files in os.walk(resolved):
198
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
199
+ for file in files:
200
+ if extension and not file.endswith(extension):
201
+ continue
202
+ full_path = os.path.join(root, file)
203
+ try:
204
+ with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
205
+ for line_no, line in enumerate(f, 1):
206
+ if query.lower() in line.lower():
207
+ rel_path = os.path.relpath(full_path, resolved)
208
+ results.append(f"{rel_path}:{line_no}: {line.strip()}")
209
+ if len(results) >= 40:
210
+ return "\n".join(results) + "\n… [Results truncated at 40 matches]"
211
+ except Exception:
212
+ continue
213
+ return "\n".join(results) if results else "No matches found."
214
+ except Exception as e:
215
+ return f"Error searching files: {e}"
216
+
217
+ @staticmethod
218
+ def get_system_info() -> str:
219
+ """Return system specs, disk space, and OS details."""
220
+ try:
221
+ total, used, free = shutil.disk_usage("/")
222
+ info = {
223
+ "os": platform.system(),
224
+ "release": platform.release(),
225
+ "machine": platform.machine(),
226
+ "python_version": platform.python_version(),
227
+ "disk_total_gb": round(total / (2**30), 2),
228
+ "disk_free_gb": round(free / (2**30), 2),
229
+ "cpu_count": os.cpu_count(),
230
+ }
231
+ return json.dumps(info, indent=2)
232
+ except Exception as e:
233
+ return f"Error getting system info: {e}"
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: notrack-cli-agent
3
+ Version: 0.6.0
4
+ Summary: Terminal agent client for NoTrack chat with supervised local tools
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests>=2.31
8
+ Requires-Dist: rich>=13.7
9
+ Requires-Dist: prompt-toolkit>=3.0
10
+
11
+ # NoTrack CLI Agent 0.6.0
12
+
13
+ Terminal chat client for NoTrack with a supervised local tool loop.
14
+
15
+ ## Install (Linux / Termux / macOS)
16
+
17
+ ```bash
18
+ python -m venv .venv
19
+ # Linux/macOS
20
+ . .venv/bin/activate
21
+ # Termux: activate the venv using the shell command above if supported
22
+ python -m pip install -U pip
23
+ python -m pip install -e .
24
+ notrack
25
+ ```
26
+
27
+ Run a single prompt:
28
+
29
+ ```bash
30
+ notrack ask "Summarize the files in this folder"
31
+ ```
32
+
33
+ ## Commands
34
+
35
+ - `/new` starts a fresh chat context.
36
+ - `/chats` attempts to list conversations visible to this HTTP session.
37
+ - `/resume <chat_id>` selects a previously known chat identifier.
38
+ - `/tools`, `/history`, `/save`, `/pwd`, `/ls`, `/cd <path>`, `/help`, `/exit`.
39
+
40
+ ## Configuration
41
+
42
+ - `NOTRACK_BASE_URL` defaults to `https://notrack.ai`
43
+ - `NOTRACK_DISPATCH_PATH` defaults to `/api/dispatch`
44
+ - `NOTRACK_MODEL` defaults to `notrack`
45
+ - `NOTRACK_MODE` defaults to `usual`
46
+
47
+ Example:
48
+
49
+ ```bash
50
+ NOTRACK_DISPATCH_PATH=/api/chat notrack
51
+ ```
52
+
53
+ ## Important protocol note
54
+
55
+ The app's dispatch POST payload is an informed compatibility attempt based on the original project and observed endpoint behavior; NoTrack's current web protocol has not been live-verified in this package. The web site may rely on session cookies, CSRF state, headers, or a changing payload. A successful GET of `/api/chats` does not guarantee that a fresh Python session sees the same anonymous account as Firefox. This client does not copy Firefox credentials or automate Firefox.
56
+
57
+ ## Safety
58
+
59
+ Local shell and file operations require confirmation by default. Read `RULE.md` before enabling any unattended mode. Treat all agent-generated commands as untrusted until reviewed.
60
+
61
+
62
+ ## v0.6.2 protocol adjustment
63
+ This build uses the observed `normal` persona, includes `regenerate: false`, and creates anonymous web-style cookies per process. This is based on a public third-party integration example, not a guaranteed official web API contract. Each CLI process creates a fresh anonymous identity; it does not restore Firefox's identity or chats automatically.
@@ -0,0 +1,11 @@
1
+ notrack_cli/__init__.py,sha256=BvQx3CzfQRKK4VLyev4xQnVCeGtri1uvTCj7qRbVWbY,49
2
+ notrack_cli/client.py,sha256=6GBi797I60iNY6lItI1pYpbl_qGN44HBPExDCBafN1M,8576
3
+ notrack_cli/main.py,sha256=9KEFC6mxYxbApbi_Aq4ZOZqA9eOTnOHgHmppY7_ujyg,35380
4
+ notrack_cli/memory.py,sha256=KHaJ99m5ypx0y6RLXWr2hVfTE38u_QClV0PmsSazcEQ,2122
5
+ notrack_cli/parser.py,sha256=I2eDpPAneDwfFmr_SIBzZHs4jlyVNzwa_OHVAy26iEU,1152
6
+ notrack_cli/tools.py,sha256=T_6U790TqdadUmjK0JM1UDMaxxCV6O8YAVWzfBx1KsA,9924
7
+ notrack_cli_agent-0.6.0.dist-info/METADATA,sha256=WPqZjxBsA_AlUTtV2qQ9GuEDp9C0OTZs2pS4IRHLGCc,2336
8
+ notrack_cli_agent-0.6.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ notrack_cli_agent-0.6.0.dist-info/entry_points.txt,sha256=ZLK2VawIjc2kOd6OCEA9_S8z1I2GJGgVH5i0-9zh6To,50
10
+ notrack_cli_agent-0.6.0.dist-info/top_level.txt,sha256=3IINcBiTUEdKBE1_Oi1cpPopqmjc5jkfehxZwkLmEwg,12
11
+ notrack_cli_agent-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ notrack = notrack_cli.main:main
@@ -0,0 +1 @@
1
+ notrack_cli