notrack-cli-agent 0.6.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.
Potentially problematic release.
This version of notrack-cli-agent might be problematic. Click here for more details.
- notrack_cli_agent-0.6.0/PKG-INFO +63 -0
- notrack_cli_agent-0.6.0/README.md +53 -0
- notrack_cli_agent-0.6.0/notrack_cli/__init__.py +2 -0
- notrack_cli_agent-0.6.0/notrack_cli/client.py +196 -0
- notrack_cli_agent-0.6.0/notrack_cli/main.py +811 -0
- notrack_cli_agent-0.6.0/notrack_cli/memory.py +73 -0
- notrack_cli_agent-0.6.0/notrack_cli/parser.py +36 -0
- notrack_cli_agent-0.6.0/notrack_cli/tools.py +233 -0
- notrack_cli_agent-0.6.0/notrack_cli_agent.egg-info/PKG-INFO +63 -0
- notrack_cli_agent-0.6.0/notrack_cli_agent.egg-info/SOURCES.txt +15 -0
- notrack_cli_agent-0.6.0/notrack_cli_agent.egg-info/dependency_links.txt +1 -0
- notrack_cli_agent-0.6.0/notrack_cli_agent.egg-info/entry_points.txt +2 -0
- notrack_cli_agent-0.6.0/notrack_cli_agent.egg-info/requires.txt +3 -0
- notrack_cli_agent-0.6.0/notrack_cli_agent.egg-info/top_level.txt +1 -0
- notrack_cli_agent-0.6.0/pyproject.toml +17 -0
- notrack_cli_agent-0.6.0/setup.cfg +4 -0
- notrack_cli_agent-0.6.0/tests/test_client.py +31 -0
|
@@ -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,53 @@
|
|
|
1
|
+
# NoTrack CLI Agent 0.6.0
|
|
2
|
+
|
|
3
|
+
Terminal chat client for NoTrack with a supervised local tool loop.
|
|
4
|
+
|
|
5
|
+
## Install (Linux / Termux / macOS)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m venv .venv
|
|
9
|
+
# Linux/macOS
|
|
10
|
+
. .venv/bin/activate
|
|
11
|
+
# Termux: activate the venv using the shell command above if supported
|
|
12
|
+
python -m pip install -U pip
|
|
13
|
+
python -m pip install -e .
|
|
14
|
+
notrack
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run a single prompt:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
notrack ask "Summarize the files in this folder"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
- `/new` starts a fresh chat context.
|
|
26
|
+
- `/chats` attempts to list conversations visible to this HTTP session.
|
|
27
|
+
- `/resume <chat_id>` selects a previously known chat identifier.
|
|
28
|
+
- `/tools`, `/history`, `/save`, `/pwd`, `/ls`, `/cd <path>`, `/help`, `/exit`.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
- `NOTRACK_BASE_URL` defaults to `https://notrack.ai`
|
|
33
|
+
- `NOTRACK_DISPATCH_PATH` defaults to `/api/dispatch`
|
|
34
|
+
- `NOTRACK_MODEL` defaults to `notrack`
|
|
35
|
+
- `NOTRACK_MODE` defaults to `usual`
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
NOTRACK_DISPATCH_PATH=/api/chat notrack
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Important protocol note
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
## Safety
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## v0.6.2 protocol adjustment
|
|
53
|
+
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,196 @@
|
|
|
1
|
+
"""NoTrack web-chat transport.
|
|
2
|
+
|
|
3
|
+
Uses the observed dispatch route and SSE-style responses. Payload shape is
|
|
4
|
+
configurable because NoTrack's private web protocol may change.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Iterator, Any
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
from requests.adapters import HTTPAdapter
|
|
16
|
+
from urllib3.util.retry import Retry
|
|
17
|
+
|
|
18
|
+
DEFAULT_BASE_URL = "https://notrack.ai"
|
|
19
|
+
DEFAULT_ENDPOINT = "/api/dispatch"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NoTrackAPIError(RuntimeError):
|
|
23
|
+
"""Remote endpoint returned an error or an unsupported response."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NoTrackClient:
|
|
27
|
+
def __init__(self, *, base_url: str | None = None, endpoint: str | None = None,
|
|
28
|
+
model: str | None = None, mode: str | None = None,
|
|
29
|
+
timeout: tuple[int, int] = (15, 180)):
|
|
30
|
+
self.base_url = (base_url or os.getenv("NOTRACK_BASE_URL", DEFAULT_BASE_URL)).rstrip("/")
|
|
31
|
+
self.endpoint_path = endpoint or os.getenv("NOTRACK_DISPATCH_PATH", DEFAULT_ENDPOINT)
|
|
32
|
+
self.endpoint = self.base_url + "/" + self.endpoint_path.lstrip("/")
|
|
33
|
+
self.model = model or os.getenv("NOTRACK_MODEL", "C")
|
|
34
|
+
self.mode = mode or os.getenv("NOTRACK_MODE", "usual")
|
|
35
|
+
self.persona = mode or os.getenv("NOTRACK_PERSONA", "normal")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self.session = self._build_session()
|
|
38
|
+
self.chat_id: str | None = None
|
|
39
|
+
self.user_id: str | None = None
|
|
40
|
+
self.last_meta: dict[str, Any] = {}
|
|
41
|
+
# New anonymous web-style identity for this CLI process.
|
|
42
|
+
self._session_id = uuid.uuid4().hex
|
|
43
|
+
self._uid = str(uuid.uuid4())
|
|
44
|
+
self.session.cookies.set("si_usr_id", self._session_id, domain="notrack.ai")
|
|
45
|
+
self.session.cookies.set("si_ses_id", self._session_id, domain="notrack.ai")
|
|
46
|
+
self.session.cookies.set("uid", self._uid, domain="notrack.ai")
|
|
47
|
+
|
|
48
|
+
def _build_session(self) -> requests.Session:
|
|
49
|
+
session = requests.Session()
|
|
50
|
+
retry = Retry(total=2, connect=2, read=0, status=2, backoff_factor=0.6,
|
|
51
|
+
status_forcelist=(429, 500, 502, 503, 504),
|
|
52
|
+
allowed_methods=frozenset({"GET"}), raise_on_status=False)
|
|
53
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
54
|
+
session.mount("https://", adapter)
|
|
55
|
+
session.mount("http://", adapter)
|
|
56
|
+
session.headers.update({
|
|
57
|
+
"Accept": "text/event-stream, application/json",
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"Origin": self.base_url,
|
|
60
|
+
"Referer": self.base_url + "/chat",
|
|
61
|
+
"User-Agent": "NoTrack-CLI/0.6 (Python requests)",
|
|
62
|
+
})
|
|
63
|
+
return session
|
|
64
|
+
|
|
65
|
+
def new_chat(self) -> None:
|
|
66
|
+
self.chat_id = None
|
|
67
|
+
|
|
68
|
+
def list_chats(self) -> list[dict[str, Any]]:
|
|
69
|
+
"""Read chats using the same HTTP session/cookies as dispatch."""
|
|
70
|
+
url = self.base_url + "/api/chats"
|
|
71
|
+
response = self.session.get(url, timeout=self.timeout[0])
|
|
72
|
+
if response.status_code >= 400:
|
|
73
|
+
raise NoTrackAPIError(f"GET /api/chats: HTTP {response.status_code}: {response.text[:500]}")
|
|
74
|
+
try:
|
|
75
|
+
data = response.json()
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
raise NoTrackAPIError("GET /api/chats did not return JSON") from exc
|
|
78
|
+
if isinstance(data, dict):
|
|
79
|
+
self.user_id = data.get("user_id") or self.user_id
|
|
80
|
+
chats = data.get("chats", [])
|
|
81
|
+
return chats if isinstance(chats, list) else []
|
|
82
|
+
if isinstance(data, list):
|
|
83
|
+
return data
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
def send_message_stream(self, user_input: str, mode: str | None = None,
|
|
87
|
+
persona: str = "normal") -> Iterator[dict[str, Any]]:
|
|
88
|
+
"""Send one prompt and yield normalized SSE/JSON events.
|
|
89
|
+
|
|
90
|
+
The field set matches the project's prior dispatch attempt, but is not
|
|
91
|
+
represented as a verified/current NoTrack contract.
|
|
92
|
+
"""
|
|
93
|
+
payload: dict[str, Any] = {
|
|
94
|
+
"user_input": user_input,
|
|
95
|
+
"mode": mode or self.mode,
|
|
96
|
+
"model": self.model,
|
|
97
|
+
"persona": self.persona,
|
|
98
|
+
"max_turns": 6,
|
|
99
|
+
"attachments": [],
|
|
100
|
+
"regenerate": False,
|
|
101
|
+
}
|
|
102
|
+
if self.chat_id:
|
|
103
|
+
payload["chat_id"] = self.chat_id
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
response = self.session.post(self.endpoint, json=payload, stream=True,
|
|
107
|
+
timeout=self.timeout)
|
|
108
|
+
except requests.RequestException as exc:
|
|
109
|
+
raise NoTrackAPIError(f"Could not connect to {self.endpoint}: {exc}") from exc
|
|
110
|
+
|
|
111
|
+
with response:
|
|
112
|
+
if response.status_code >= 400:
|
|
113
|
+
excerpt = response.text[:1200]
|
|
114
|
+
raise NoTrackAPIError(f"POST {self.endpoint_path}: HTTP {response.status_code}: {excerpt}")
|
|
115
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
116
|
+
if "json" in content_type and "event-stream" not in content_type:
|
|
117
|
+
try:
|
|
118
|
+
obj = response.json()
|
|
119
|
+
except ValueError as exc:
|
|
120
|
+
raise NoTrackAPIError("Endpoint returned invalid JSON") from exc
|
|
121
|
+
if isinstance(obj, dict) and obj.get("type") == "error":
|
|
122
|
+
raise NoTrackAPIError(str(obj.get("content") or obj))
|
|
123
|
+
if isinstance(obj, dict):
|
|
124
|
+
yield from self._handle_event(obj)
|
|
125
|
+
else:
|
|
126
|
+
yield {"type": "message", "content": json.dumps(obj, ensure_ascii=False)}
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
for event in self._iter_sse(response):
|
|
130
|
+
if event.get("type") == "error":
|
|
131
|
+
raise NoTrackAPIError(str(event.get("content") or event))
|
|
132
|
+
yield from self._handle_event(event)
|
|
133
|
+
|
|
134
|
+
def _handle_event(self, event: dict[str, Any]) -> Iterator[dict[str, Any]]:
|
|
135
|
+
kind = str(event.get("type", ""))
|
|
136
|
+
if kind in {"chat_meta", "metadata", "meta"}:
|
|
137
|
+
self.last_meta.update(event)
|
|
138
|
+
if event.get("chat_id"):
|
|
139
|
+
self.chat_id = str(event["chat_id"])
|
|
140
|
+
if event.get("user_id"):
|
|
141
|
+
self.user_id = str(event["user_id"])
|
|
142
|
+
yield event
|
|
143
|
+
elif kind in {"delta", "token", "text_delta", "content_block_delta"}:
|
|
144
|
+
chunk = event.get("chunk", event.get("content", event.get("text", "")))
|
|
145
|
+
if isinstance(chunk, dict):
|
|
146
|
+
chunk = chunk.get("text", "")
|
|
147
|
+
if chunk:
|
|
148
|
+
yield {"type": "delta", "chunk": str(chunk), "raw": event}
|
|
149
|
+
elif kind in {"message", "final", "done", "response"}:
|
|
150
|
+
content = event.get("content", event.get("message", event.get("text", "")))
|
|
151
|
+
if isinstance(content, dict):
|
|
152
|
+
content = content.get("content", content.get("text", ""))
|
|
153
|
+
if content:
|
|
154
|
+
yield {"type": "message", "content": str(content), "raw": event}
|
|
155
|
+
else:
|
|
156
|
+
yield event
|
|
157
|
+
elif kind:
|
|
158
|
+
yield event
|
|
159
|
+
elif any(k in event for k in ("content", "text", "chunk")):
|
|
160
|
+
content = event.get("chunk", event.get("content", event.get("text", "")))
|
|
161
|
+
if content:
|
|
162
|
+
yield {"type": "delta", "chunk": str(content), "raw": event}
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _iter_sse(response: requests.Response) -> Iterator[dict[str, Any]]:
|
|
166
|
+
"""Read data lines, allowing multiple data: lines per SSE event."""
|
|
167
|
+
data_lines: list[str] = []
|
|
168
|
+
for raw in response.iter_lines(decode_unicode=True):
|
|
169
|
+
if raw is None:
|
|
170
|
+
continue
|
|
171
|
+
line = raw if isinstance(raw, str) else raw.decode("utf-8", errors="replace")
|
|
172
|
+
if not line:
|
|
173
|
+
if data_lines:
|
|
174
|
+
parsed = NoTrackClient._decode_sse_data("\n".join(data_lines))
|
|
175
|
+
if parsed is not None:
|
|
176
|
+
yield parsed
|
|
177
|
+
data_lines.clear()
|
|
178
|
+
continue
|
|
179
|
+
if line.startswith(":"):
|
|
180
|
+
continue
|
|
181
|
+
if line.startswith("data:"):
|
|
182
|
+
data_lines.append(line[5:].lstrip())
|
|
183
|
+
if data_lines:
|
|
184
|
+
parsed = NoTrackClient._decode_sse_data("\n".join(data_lines))
|
|
185
|
+
if parsed is not None:
|
|
186
|
+
yield parsed
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _decode_sse_data(raw: str) -> dict[str, Any] | None:
|
|
190
|
+
if not raw or raw == "[DONE]":
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
obj = json.loads(raw)
|
|
194
|
+
except ValueError:
|
|
195
|
+
return {"type": "delta", "chunk": raw}
|
|
196
|
+
return obj if isinstance(obj, dict) else {"type": "message", "content": str(obj)}
|