panopticon-cli 1.1.1__tar.gz → 1.1.2__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.
Files changed (23) hide show
  1. {panopticon_cli-1.1.1/panopticon_cli.egg-info → panopticon_cli-1.1.2}/PKG-INFO +1 -1
  2. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/cli.py +45 -56
  3. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2/panopticon_cli.egg-info}/PKG-INFO +1 -1
  4. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/pyproject.toml +3 -3
  5. panopticon_cli-1.1.2/tests/test_cli.py +63 -0
  6. panopticon_cli-1.1.1/tests/test_cli.py +0 -15
  7. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/LICENSE +0 -0
  8. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/README.md +0 -0
  9. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/__init__.py +0 -0
  10. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/memory.py +0 -0
  11. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/observer.py +0 -0
  12. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/policies.py +0 -0
  13. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/sentinel.py +0 -0
  14. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/SOURCES.txt +0 -0
  15. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/dependency_links.txt +0 -0
  16. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/entry_points.txt +0 -0
  17. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/requires.txt +0 -0
  18. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/top_level.txt +0 -0
  19. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/setup.cfg +0 -0
  20. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_memory.py +0 -0
  21. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_observer.py +0 -0
  22. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_policies.py +0 -0
  23. {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_sentinel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -36,6 +36,7 @@ class CLIWrapper:
36
36
  self.running = False
37
37
  self.lock = threading.Lock()
38
38
  self.threads = []
39
+ self.stdin_reader = None
39
40
 
40
41
  def run(self):
41
42
  self.running = True
@@ -46,7 +47,8 @@ class CLIWrapper:
46
47
 
47
48
  print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
48
49
  print(
49
- f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), AdversarialLogic"
50
+ f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), "
51
+ f"AdversarialLogic"
50
52
  )
51
53
 
52
54
  # Flaw 2 Fix: Removing text=True to read raw binary bytes
@@ -63,19 +65,21 @@ class CLIWrapper:
63
65
  t_out.start()
64
66
  self.threads.append(t_out)
65
67
 
66
- t_in = threading.Thread(target=self._forward_stdin, daemon=True)
67
- t_in.start()
68
- self.threads.append(t_in)
69
-
70
68
  t_eval = threading.Thread(target=self._eval_loop, daemon=True)
71
69
  t_eval.start()
72
70
  self.threads.append(t_eval)
73
71
 
72
+ t_in = threading.Thread(target=self._read_stdin, daemon=True)
73
+ t_in.start()
74
+ self.threads.append(t_in)
75
+
74
76
  # Register cleanup on exit
75
77
  atexit.register(self._cleanup)
76
78
 
77
79
  try:
78
80
  self.process.wait()
81
+ except KeyboardInterrupt:
82
+ pass
79
83
  finally:
80
84
  self.running = False
81
85
  self._cleanup()
@@ -84,7 +88,7 @@ class CLIWrapper:
84
88
  """Cleanly shutdown all threads and resources."""
85
89
  self.running = False
86
90
 
87
- # Close subprocess pipes
91
+ # Close subprocess pipes FIRST before thread cleanup
88
92
  if self.process:
89
93
  try:
90
94
  if self.process.stdin and not self.process.stdin.closed:
@@ -113,52 +117,6 @@ class CLIWrapper:
113
117
  if thread.is_alive():
114
118
  thread.join(timeout=1)
115
119
 
116
- def _forward_stdin(self):
117
- """Forward stdin to subprocess, with non-blocking checks."""
118
- try:
119
- while self.running and self.process and self.process.poll() is None:
120
- try:
121
- # Windows-safe non-blocking stdin read
122
- if sys.platform == "win32":
123
- # On Windows, use a small sleep to avoid 100% CPU
124
- time.sleep(0.05)
125
- # Try non-blocking read using a different approach
126
- if sys.stdin and not sys.stdin.closed:
127
- try:
128
- # This will still block, but with timeout via select on Unix
129
- line = sys.stdin.buffer.readline()
130
- if line:
131
- if self.process.stdin and not self.process.stdin.closed:
132
- self.process.stdin.write(line)
133
- self.process.stdin.flush()
134
- else:
135
- break # EOF
136
- except (EOFError, OSError, ValueError):
137
- break
138
- else:
139
- # Unix/Linux: use select for non-blocking check
140
- import select
141
-
142
- ready, _, _ = select.select([sys.stdin], [], [], 0.1)
143
- if ready:
144
- line = sys.stdin.buffer.readline()
145
- if line:
146
- if self.process.stdin and not self.process.stdin.closed:
147
- self.process.stdin.write(line)
148
- self.process.stdin.flush()
149
- else:
150
- break # EOF
151
- except (EOFError, OSError, ValueError, BrokenPipeError):
152
- break
153
- except Exception:
154
- pass
155
- finally:
156
- try:
157
- if self.process and self.process.stdin and not self.process.stdin.closed:
158
- self.process.stdin.close()
159
- except Exception:
160
- pass
161
-
162
120
  def _read_stdout(self):
163
121
  # Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
164
122
  decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
@@ -183,11 +141,40 @@ class CLIWrapper:
183
141
  pass
184
142
  finally:
185
143
  try:
186
- if self.process and self.process.stdout and not self.process.stdout.closed:
144
+ if (
145
+ self.process
146
+ and self.process.stdout
147
+ and (not self.process.stdout.closed)
148
+ ):
187
149
  self.process.stdout.close()
188
150
  except Exception:
189
151
  pass
190
152
 
153
+ def _read_stdin(self):
154
+ try:
155
+ while self.running and self.process and self.process.poll() is None:
156
+ try:
157
+ data = sys.stdin.buffer.read1(1024)
158
+ if not data:
159
+ break
160
+ if self.process.stdin and not self.process.stdin.closed:
161
+ self.process.stdin.write(data)
162
+ self.process.stdin.flush()
163
+ except (OSError, ValueError, BrokenPipeError):
164
+ break
165
+ except Exception:
166
+ pass
167
+ finally:
168
+ try:
169
+ if (
170
+ self.process
171
+ and self.process.stdin
172
+ and (not self.process.stdin.closed)
173
+ ):
174
+ self.process.stdin.close()
175
+ except Exception:
176
+ pass
177
+
191
178
  def _eval_loop(self):
192
179
  while self.running and self.process and self.process.poll() is None:
193
180
  time.sleep(15)
@@ -214,14 +201,15 @@ class CLIWrapper:
214
201
  print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
215
202
  print(f"[REASON]: {e.args[0]}")
216
203
  print(
217
- f"[LIVE INJECTION]: Interrupting agent and injecting correction...\n"
204
+ f"[LIVE INJECTION]: Interrupting agent and injecting "
205
+ f"correction...\n"
218
206
  )
219
207
 
220
208
  # Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
221
209
  try:
222
210
  if sys.platform != "win32":
223
211
  self.process.send_signal(signal.SIGINT)
224
- # Give the agent a split second to catch the interrupt before writing to stdin
212
+ # Give the agent a split second to catch the interrupt
225
213
  time.sleep(0.5)
226
214
  except Exception:
227
215
  pass
@@ -234,7 +222,8 @@ class CLIWrapper:
234
222
  self.process.stdin.flush()
235
223
  except Exception as ex:
236
224
  print(
237
- f"[ERROR] Live injection failed or agent deadlocked: {ex}. Hard terminating."
225
+ f"[ERROR] Live injection failed or agent deadlocked: "
226
+ f"{ex}. Hard terminating."
238
227
  )
239
228
  self.process.terminate()
240
229
  self.running = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "panopticon-cli"
7
- version = "1.1.1"
7
+ version = "1.1.2"
8
8
  description = "The Cognitive Immune System for Autonomous CLI Agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -43,5 +43,5 @@ dev = [
43
43
  panopticon = "panopticon.cli:main"
44
44
 
45
45
  [tool.black]
46
- target-versions = ["py39", "py310", "py311", "py312"]
47
- line-length = 88
46
+ target_version = ["py39", "py310", "py311", "py312"]
47
+ line_length = 88
@@ -0,0 +1,63 @@
1
+ import io
2
+ import sys
3
+
4
+ from panopticon.cli import CLIWrapper, strip_ansi
5
+
6
+
7
+ class DummyStdinStream:
8
+ def __init__(self, data: bytes):
9
+ self.buffer = io.BytesIO(data)
10
+
11
+
12
+ class DummyProcessStdin:
13
+ def __init__(self):
14
+ self.written = bytearray()
15
+ self.closed = False
16
+
17
+ def write(self, data: bytes):
18
+ self.written.extend(data)
19
+
20
+ def flush(self):
21
+ pass
22
+
23
+ def close(self):
24
+ self.closed = True
25
+
26
+
27
+ class DummyProcess:
28
+ def __init__(self):
29
+ self.stdin = DummyProcessStdin()
30
+ self.stdout = io.BytesIO()
31
+
32
+ def poll(self):
33
+ return None
34
+
35
+
36
+ def test_strip_ansi():
37
+ # Basic red color text
38
+ colored_text = "\x1b[31mFatal Error\x1b[0m: File not found"
39
+ assert strip_ansi(colored_text) == "Fatal Error: File not found"
40
+
41
+ # Complex formatting (Bold + Green)
42
+ complex_text = "\x1b[1m\x1b[32mSuccess\x1b[0m"
43
+ assert strip_ansi(complex_text) == "Success"
44
+
45
+ # Loading spinner edge case
46
+ spinner_text = "\x1b[?25l\x1b[2K\x1b[1G⠋ Loading..."
47
+ assert "Loading..." in strip_ansi(spinner_text)
48
+
49
+
50
+ def test_read_stdin_forwards_input():
51
+ wrapper = CLIWrapper(["dummy"])
52
+ wrapper.running = True
53
+ wrapper.process = DummyProcess()
54
+
55
+ original_stdin = sys.stdin
56
+ try:
57
+ sys.stdin = DummyStdinStream(b"hello world\n")
58
+ wrapper._read_stdin()
59
+ finally:
60
+ sys.stdin = original_stdin
61
+
62
+ assert wrapper.process.stdin.written == b"hello world\n"
63
+ assert wrapper.process.stdin.closed is True
@@ -1,15 +0,0 @@
1
- from panopticon.cli import strip_ansi
2
-
3
-
4
- def test_strip_ansi():
5
- # Basic red color text
6
- colored_text = "\x1b[31mFatal Error\x1b[0m: File not found"
7
- assert strip_ansi(colored_text) == "Fatal Error: File not found"
8
-
9
- # Complex formatting (Bold + Green)
10
- complex_text = "\x1b[1m\x1b[32mSuccess\x1b[0m"
11
- assert strip_ansi(complex_text) == "Success"
12
-
13
- # Loading spinner edge case
14
- spinner_text = "\x1b[?25l\x1b[2K\x1b[1G⠋ Loading..."
15
- assert "Loading..." in strip_ansi(spinner_text)
File without changes
File without changes
File without changes