panopticon-cli 1.0.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.
panopticon/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .observer import PanopticonObserver
2
+ from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
3
+ from .memory import PersistentMemory
4
+
5
+ __all__ = [
6
+ "PanopticonObserver",
7
+ "AdversarialLogicCheck",
8
+ "AntiLoopPolicy",
9
+ "BlacklistPolicy",
10
+ "PersistentMemory"
11
+ ]
panopticon/cli.py ADDED
@@ -0,0 +1,164 @@
1
+ import subprocess
2
+ import threading
3
+ import sys
4
+ import time
5
+ import argparse
6
+ import re
7
+ import os
8
+ import signal
9
+ import codecs
10
+ from typing import List
11
+ from .observer import PanopticonObserver, InterventionException
12
+ from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
13
+
14
+ def strip_ansi(text: str) -> str:
15
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
16
+ return ansi_escape.sub('', text)
17
+
18
+ class CLIWrapper:
19
+ def __init__(self, command: List[str]):
20
+ self.command = command
21
+ target_agent = self.command[0] if self.command else "unknown"
22
+
23
+ self.observer = PanopticonObserver(policies=[
24
+ BlacklistPolicy(forbidden_patterns=["rm -rf /", "DROP TABLE"]),
25
+ AntiLoopPolicy(window=3, threshold=0.85),
26
+ AdversarialLogicCheck(target_agent=target_agent)
27
+ ])
28
+
29
+ self.process = None
30
+ self.buffer = []
31
+ self.running = False
32
+ self.lock = threading.Lock()
33
+
34
+ def run(self):
35
+ self.running = True
36
+ env = os.environ.copy()
37
+ env["PYTHONUNBUFFERED"] = "1"
38
+ env["FORCE_COLOR"] = "1"
39
+ env["CLICOLOR_FORCE"] = "1"
40
+
41
+ print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
42
+ print(f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), AdversarialLogic")
43
+
44
+ # Flaw 2 Fix: Removing text=True to read raw binary bytes
45
+ self.process = subprocess.Popen(
46
+ self.command,
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.STDOUT,
49
+ stdin=subprocess.PIPE,
50
+ bufsize=0,
51
+ env=env
52
+ )
53
+
54
+ t_out = threading.Thread(target=self._read_stdout)
55
+ t_out.daemon = True
56
+ t_out.start()
57
+
58
+ t_in = threading.Thread(target=self._forward_stdin)
59
+ t_in.daemon = True
60
+ t_in.start()
61
+
62
+ t_eval = threading.Thread(target=self._eval_loop)
63
+ t_eval.daemon = True
64
+ t_eval.start()
65
+
66
+ self.process.wait()
67
+ self.running = False
68
+
69
+ def _forward_stdin(self):
70
+ try:
71
+ while self.running and self.process.poll() is None:
72
+ # Read binary from stdin to pass natively to process
73
+ line = sys.stdin.buffer.readline()
74
+ if line:
75
+ self.process.stdin.write(line)
76
+ self.process.stdin.flush()
77
+ except Exception:
78
+ pass
79
+
80
+ def _read_stdout(self):
81
+ # Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
82
+ decoder = codecs.getincrementaldecoder('utf-8')(errors='replace')
83
+ try:
84
+ while self.running and self.process.poll() is None:
85
+ byte_chunk = self.process.stdout.read(1)
86
+ if not byte_chunk:
87
+ break
88
+
89
+ # Mirror raw bytes to actual terminal seamlessly
90
+ sys.stdout.buffer.write(byte_chunk)
91
+ sys.stdout.buffer.flush()
92
+
93
+ char_str = decoder.decode(byte_chunk)
94
+ if char_str:
95
+ with self.lock:
96
+ self.buffer.append(char_str)
97
+ except Exception:
98
+ pass
99
+
100
+ def _eval_loop(self):
101
+ while self.running and self.process.poll() is None:
102
+ time.sleep(15)
103
+
104
+ with self.lock:
105
+ if not self.buffer:
106
+ continue
107
+ full_text = "".join(self.buffer)
108
+
109
+ # Flaw 1 Fix: Sliding window overlap to prevent bridging blindness
110
+ overlap = full_text[-100:] if len(full_text) > 100 else ""
111
+ self.buffer.clear()
112
+ if overlap:
113
+ self.buffer.append(overlap)
114
+
115
+ clean_text = strip_ansi(full_text)
116
+ self.observer.log_action("CLI_Agent", clean_text, "cli_execution", len(clean_text) // 4)
117
+
118
+ try:
119
+ self.observer._evaluate_state("CLI_Agent")
120
+ except InterventionException as e:
121
+ print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
122
+ print(f"[REASON]: {e.args[0]}")
123
+ print(f"[LIVE INJECTION]: Interrupting agent and injecting correction...\n")
124
+
125
+ # Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
126
+ try:
127
+ if sys.platform != "win32":
128
+ self.process.send_signal(signal.SIGINT)
129
+ # Give the agent a split second to catch the interrupt before writing to stdin
130
+ time.sleep(0.5)
131
+ except Exception:
132
+ pass
133
+
134
+ try:
135
+ # Write the injection payload as binary
136
+ payload = (e.course_correction + "\n").encode('utf-8')
137
+ self.process.stdin.write(payload)
138
+ self.process.stdin.flush()
139
+ except Exception as ex:
140
+ print(f"[ERROR] Live injection failed or agent deadlocked: {ex}. Hard terminating.")
141
+ self.process.terminate()
142
+ self.running = False
143
+ break
144
+
145
+ def main():
146
+ try:
147
+ from dotenv import load_dotenv
148
+ load_dotenv()
149
+ except ImportError:
150
+ pass
151
+
152
+ parser = argparse.ArgumentParser(description="Panopticon: Production-Grade Immune System for AI CLIs")
153
+ parser.add_argument("command", nargs=argparse.REMAINDER, help="The command to run, e.g., 'claude'")
154
+ args = parser.parse_args()
155
+
156
+ if not args.command:
157
+ print("Usage: panopticon [command]")
158
+ sys.exit(1)
159
+
160
+ wrapper = CLIWrapper(args.command)
161
+ wrapper.run()
162
+
163
+ if __name__ == "__main__":
164
+ main()
panopticon/memory.py ADDED
@@ -0,0 +1,56 @@
1
+ import sqlite3
2
+ import os
3
+ import re
4
+ from typing import List, Dict
5
+
6
+ class PersistentMemory:
7
+ """Production-grade procedural memory using keyword similarity routing."""
8
+ def __init__(self, db_path="panopticon_memory.db"):
9
+ self.db_path = db_path
10
+ self._init_db()
11
+
12
+ def _init_db(self):
13
+ # Flaw 3 Fix: Enable WAL and connection timeout for multi-process concurrency
14
+ with sqlite3.connect(self.db_path, timeout=10.0) as conn:
15
+ conn.execute("PRAGMA journal_mode=WAL;")
16
+ conn.execute("""
17
+ CREATE TABLE IF NOT EXISTS failures (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
20
+ reason TEXT,
21
+ correction TEXT
22
+ )
23
+ """)
24
+
25
+ def record_failure(self, reason: str, correction: str):
26
+ with sqlite3.connect(self.db_path, timeout=10.0) as conn:
27
+ conn.execute(
28
+ "INSERT INTO failures (reason, correction) VALUES (?, ?)",
29
+ (reason, correction)
30
+ )
31
+
32
+ def _extract_keywords(self, text: str) -> set:
33
+ words = re.findall(r'\b[a-zA-Z]{4,}\b', text.lower())
34
+ return set(words)
35
+
36
+ def get_relevant_failures(self, current_context: str, limit=3) -> List[Dict]:
37
+ """Retrieves past failures that have the highest word overlap with the current context."""
38
+ current_keywords = self._extract_keywords(current_context)
39
+ if not current_keywords:
40
+ return []
41
+
42
+ with sqlite3.connect(self.db_path, timeout=10.0) as conn:
43
+ cursor = conn.execute("SELECT reason, correction FROM failures")
44
+ all_failures = cursor.fetchall()
45
+
46
+ scored_failures = []
47
+ for reason, correction in all_failures:
48
+ reason_keywords = self._extract_keywords(reason)
49
+ # Calculate Jaccard-like overlap score
50
+ overlap = len(current_keywords.intersection(reason_keywords))
51
+ if overlap > 0:
52
+ scored_failures.append((overlap, {"reason": reason, "correction": correction}))
53
+
54
+ # Sort by highest overlap, then take top N
55
+ scored_failures.sort(key=lambda x: x[0], reverse=True)
56
+ return [f[1] for f in scored_failures[:limit]]
panopticon/observer.py ADDED
@@ -0,0 +1,64 @@
1
+ import json
2
+ import os
3
+ from typing import List, Dict, Callable
4
+ from .policies import BasePolicy
5
+
6
+ class InterventionException(Exception):
7
+ def __init__(self, message: str, course_correction: str):
8
+ super().__init__(message)
9
+ self.course_correction = course_correction
10
+
11
+ class PanopticonObserver:
12
+ def __init__(self, policies: List[BasePolicy] = None, telemetry_file: str = "panopticon_telemetry.jsonl"):
13
+ self.policies = policies or []
14
+ self.telemetry_stream = []
15
+ self.telemetry_file = telemetry_file
16
+
17
+ if os.path.exists(self.telemetry_file):
18
+ os.remove(self.telemetry_file)
19
+
20
+ def _broadcast(self, event_type: str, data: Dict):
21
+ """Writes to a JSONL file so the Pantheon OS Dashboard can tail it in real-time."""
22
+ payload = {"event": event_type, **data}
23
+ with open(self.telemetry_file, "a") as f:
24
+ f.write(json.dumps(payload) + "\n")
25
+
26
+ def log_action(self, agent_name: str, thought: str, action: str, tokens_used: int):
27
+ entry = {
28
+ "agent": agent_name,
29
+ "thought": thought,
30
+ "action": action,
31
+ "tokens": tokens_used
32
+ }
33
+ # Flaw 2 Fix: Bounded memory to prevent leaks
34
+ self.telemetry_stream.append(entry)
35
+ if len(self.telemetry_stream) > 20:
36
+ self.telemetry_stream.pop(0)
37
+ self._broadcast("step", entry)
38
+ self._evaluate_state(agent_name)
39
+
40
+ def _evaluate_state(self, agent_name: str):
41
+ for policy in self.policies:
42
+ is_violating, reason, correction = policy.evaluate(self.telemetry_stream)
43
+ if is_violating:
44
+ self._broadcast("guillotine", {
45
+ "agent": agent_name,
46
+ "reason": reason,
47
+ "correction": correction
48
+ })
49
+ self._trigger_guillotine(agent_name, reason, correction)
50
+
51
+ def _trigger_guillotine(self, agent_name: str, reason: str, correction: str):
52
+ raise InterventionException(message=reason, course_correction=correction)
53
+
54
+ def watch(self, agent_name: str):
55
+ def decorator(func: Callable):
56
+ def wrapper(*args, **kwargs):
57
+ self._broadcast("start", {"agent": agent_name})
58
+ try:
59
+ return func(*args, **kwargs)
60
+ except InterventionException as e:
61
+ self._broadcast("interrupt", {"agent": agent_name, "correction": e.course_correction})
62
+ return {"status": "interrupted", "correction": e.course_correction}
63
+ return wrapper
64
+ return decorator
panopticon/policies.py ADDED
@@ -0,0 +1,53 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Dict, Tuple
3
+ from .sentinel import Sentinel
4
+
5
+ class BasePolicy(ABC):
6
+ @abstractmethod
7
+ def evaluate(self, telemetry_stream: List[Dict]) -> Tuple[bool, str, str]:
8
+ pass
9
+
10
+
11
+ import difflib
12
+
13
+ class AntiLoopPolicy(BasePolicy):
14
+ def __init__(self, window: int = 3, threshold: float = 0.85):
15
+ self.window = window
16
+ self.threshold = threshold
17
+
18
+ def evaluate(self, telemetry_stream: List[Dict]) -> Tuple[bool, str, str]:
19
+ if len(telemetry_stream) < self.window:
20
+ return False, "", ""
21
+
22
+ recent = [entry.get("thought", "").strip() for entry in telemetry_stream[-self.window:]]
23
+ if not recent[0]:
24
+ return False, "", ""
25
+
26
+ # Flaw 3 Fix: Fuzzy match chunks (≥85% similar) to ignore timestamps/spinners
27
+ ratios = [difflib.SequenceMatcher(None, recent[0], t).ratio() for t in recent]
28
+
29
+ if all(r >= self.threshold for r in ratios):
30
+ return True, "Heuristic: Fuzzy repetition loop detected", "[SYSTEM OVERRIDE] You are repeating the exact same actions. Try a different strategy."
31
+ return False, "", ""
32
+
33
+ class BlacklistPolicy(BasePolicy):
34
+ def __init__(self, forbidden_patterns: List[str]):
35
+ self.forbidden_patterns = forbidden_patterns
36
+
37
+ def evaluate(self, telemetry_stream: List[Dict]) -> Tuple[bool, str, str]:
38
+ if not telemetry_stream:
39
+ return False, "", ""
40
+ latest = telemetry_stream[-1].get("thought", "").lower()
41
+ for pattern in self.forbidden_patterns:
42
+ if pattern.lower() in latest:
43
+ return True, f"Blacklisted pattern detected: '{pattern}'", f"[SYSTEM OVERRIDE] You triggered a forbidden action ({pattern}). Reverse course immediately."
44
+ return False, "", ""
45
+
46
+ class AdversarialLogicCheck(BasePolicy):
47
+ def __init__(self, target_agent: str = "claude"):
48
+ self.sentinel = Sentinel(target_agent=target_agent)
49
+
50
+ def evaluate(self, telemetry_stream: List[Dict]) -> Tuple[bool, str, str]:
51
+ if len(telemetry_stream) >= 2:
52
+ return self.sentinel.evaluate_trajectory(telemetry_stream)
53
+ return False, "", ""
panopticon/sentinel.py ADDED
@@ -0,0 +1,122 @@
1
+ import json
2
+ import os
3
+ from typing import List, Dict, Tuple
4
+ from .memory import PersistentMemory
5
+
6
+ class Sentinel:
7
+ """
8
+ The Universal Meta-Agent.
9
+ Dynamically routes its semantic evaluation to match the API of the agent it is observing.
10
+ """
11
+ def __init__(self, target_agent: str = "claude"):
12
+ self.target_agent = target_agent.lower()
13
+ self.memory = PersistentMemory()
14
+
15
+ def evaluate_trajectory(self, telemetry_stream: List[Dict]) -> Tuple[bool, str, str]:
16
+ if len(telemetry_stream) < 2:
17
+ return False, "", ""
18
+
19
+ recent_chunks = [e.get("thought", "")[:500] for e in telemetry_stream[-3:]]
20
+ context_str = " ".join(recent_chunks)
21
+
22
+ # Pull dynamically relevant failures based on current context
23
+ past_failures = self.memory.get_relevant_failures(current_context=context_str, limit=3)
24
+
25
+ prompt = f"""
26
+ Execution trace:
27
+ {json.dumps(recent_chunks)}
28
+
29
+ Relevant past failures (Do not repeat these mistakes):
30
+ {json.dumps(past_failures)}
31
+
32
+ Is the agent stuck in a loop, failing logical progress, or repeating a past recorded failure?
33
+ Respond ONLY in JSON:
34
+ {{"is_failing": true/false, "reason": "why if true", "correction_prompt": "directive prompt to inject to break the loop or correct the logic. empty if not failing"}}
35
+ """
36
+ try:
37
+ result_text = self._route_to_llm(prompt)
38
+
39
+ # Flaw 4 Fix: Robust regex JSON extraction ignoring conversational fluff
40
+ import re
41
+ match = re.search(r'\{.*\}', result_text, re.DOTALL)
42
+ if match:
43
+ result_text = match.group(0)
44
+ else:
45
+ return False, "", ""
46
+
47
+ data = json.loads(result_text)
48
+
49
+ if data.get("is_failing"):
50
+ reason = data.get("reason", "Failure detected")
51
+ correction = data.get("correction_prompt", "Discard approach.")
52
+ self.memory.record_failure(reason, correction)
53
+ return True, reason, correction
54
+ return False, "", ""
55
+
56
+ except Exception as e:
57
+ # Flaw 4 Fix: Don't fail silently on critical API configuration errors
58
+ error_str = str(e).lower()
59
+ if "api_key" in error_str or "auth" in error_str or "not found" in error_str:
60
+ raise RuntimeError(f"\n[PANOPTICON FATAL ERROR] Missing or invalid API key for {self.target_agent}: {e}")
61
+
62
+ # For random network timeouts, ignore and try again next loop
63
+ return False, "", ""
64
+
65
+ def _route_to_llm(self, prompt: str) -> str:
66
+ """Dynamically switch the meta-agent API based on the target CLI command, with intelligent fallbacks."""
67
+ agent = self.target_agent
68
+
69
+ # 1. Explicit matching based on command name
70
+ if "agy" in agent or "gemini" in agent:
71
+ if os.environ.get("GEMINI_API_KEY"): return self._call_gemini(prompt)
72
+ elif "gpt" in agent or "codex" in agent or "openai" in agent:
73
+ if os.environ.get("OPENAI_API_KEY"): return self._call_openai(prompt)
74
+ elif "claude" in agent:
75
+ if os.environ.get("ANTHROPIC_API_KEY"): return self._call_anthropic(prompt)
76
+
77
+ # 2. Flaw 3 Fix: Intelligent Fallback if explicit match fails or agent is generic (e.g. 'python')
78
+ if os.environ.get("ANTHROPIC_API_KEY"):
79
+ return self._call_anthropic(prompt)
80
+ elif os.environ.get("GEMINI_API_KEY"):
81
+ return self._call_gemini(prompt)
82
+ elif os.environ.get("OPENAI_API_KEY"):
83
+ return self._call_openai(prompt)
84
+ else:
85
+ raise RuntimeError("\n[PANOPTICON FATAL] No API keys found! Please set ANTHROPIC_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY.")
86
+
87
+ def _call_anthropic(self, prompt: str) -> str:
88
+ from anthropic import Anthropic
89
+ client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
90
+ resp = client.messages.create(
91
+ model="claude-3-5-haiku-20241022",
92
+ max_tokens=150,
93
+ temperature=0.0,
94
+ system="Strict meta-agent. Output JSON.",
95
+ messages=[{"role": "user", "content": prompt}]
96
+ )
97
+ return resp.content[0].text
98
+
99
+ def _call_openai(self, prompt: str) -> str:
100
+ from openai import OpenAI
101
+ client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
102
+ resp = client.chat.completions.create(
103
+ model="gpt-4o-mini",
104
+ response_format={ "type": "json_object" },
105
+ messages=[
106
+ {"role": "system", "content": "Strict meta-agent. Output JSON."},
107
+ {"role": "user", "content": prompt}
108
+ ]
109
+ )
110
+ return resp.choices[0].message.content
111
+
112
+ def _call_gemini(self, prompt: str) -> str:
113
+ import google.generativeai as genai
114
+ genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
115
+ model = genai.GenerativeModel('gemini-1.5-flash')
116
+ resp = model.generate_content(
117
+ "Strict meta-agent. Output JSON.\n\n" + prompt,
118
+ generation_config=genai.types.GenerationConfig(
119
+ response_mime_type="application/json",
120
+ )
121
+ )
122
+ return resp.text
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: panopticon-cli
3
+ Version: 1.0.0
4
+ Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
+ Author: Ak
6
+ License: MIT
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Build Tools
15
+ Classifier: Topic :: System :: Systems Administration
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: pydantic>=2.0.0
20
+ Requires-Dist: openai>=1.0.0
21
+ Requires-Dist: numpy>=1.24.0
22
+ Requires-Dist: anthropic>=0.30.0
23
+ Requires-Dist: google-generativeai>=0.5.0
24
+ Requires-Dist: python-dotenv>=1.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: black>=23.0.0; extra == "dev"
28
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # PANOPTICON 👁️
32
+
33
+ > **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
34
+
35
+ [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
36
+ [![PyPI Downloads](https://static.pepy.tech/badge/panopticon-cli)](https://pypi.org/project/panopticon-cli/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
38
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
39
+ [![CI](https://github.com/ak/panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak/panopticon/actions)
40
+
41
+ Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
42
+
43
+ **Panopticon** is a zero-latency, non-blocking wrapper that silently watches your agent's terminal output. When the AI inevitably tries to do something incredibly stupid, Panopticon's **State Guillotine** drops.
44
+
45
+ It intercepts the rogue process, uses a Meta-Agent to figure out why your AI is crying, and **forcefully injects the correction directly into the agent's live `stdin` stream** like a disappointed senior developer taking over the keyboard. Oh, and it saves that failure to a SQLite database so the agent never makes the exact same mistake twice.
46
+
47
+ ## Why you need this
48
+
49
+ - **Native TTY Wrapping:** Slap it in front of any CLI. Loading spinners, ANSI colors, and interactive prompts still work perfectly. We just spy on them natively.
50
+ - **The Policy Cascade (Iron Dome):**
51
+ - **Level 1 (Blacklist):** Instant, zero-cost kills for destructive actions. Because your AI *will* try to drop your production database eventually.
52
+ - **Level 2 (Fuzzy Heuristics):** Zero-cost math thresholds that catch repetitive loops. Stops the agent from running `cat missing_file.py` 300 times in a row.
53
+ - **Level 3 (Universal Semantic Logic):** Streams a sliding-window of the terminal to a Meta-Agent (Claude, GPT, or Gemini) to judge your sub-agent's poor life choices.
54
+ - **True Live Injection:** We don't just kill the agent and leave you hanging. Panopticon literally types the course-correction prompt into the interactive terminal for you, saving the session.
55
+ - **Persistent Semantic Memory:** AI agents have goldfish memory. We use SQLite and Jaccard keyword routing to permanently scar them with their past failures so they actually learn.
56
+
57
+ ## Quick Start
58
+
59
+ ```bash
60
+ # Install globally from PyPI
61
+ pip install panopticon-cli
62
+
63
+ # Export your preferred API Key (Panopticon dynamically routes to whatever you actually pay for)
64
+ export OPENAI_API_KEY="sk-..."
65
+ # OR export ANTHROPIC_API_KEY="..."
66
+ # OR export GEMINI_API_KEY="..."
67
+
68
+ # Slap it in front of your agent!
69
+ panopticon claude
70
+ # OR
71
+ panopticon agy
72
+ ```
73
+
74
+ See [USAGE.md](USAGE.md) for deeper configuration so you can start dropping the Guillotine.
@@ -0,0 +1,12 @@
1
+ panopticon/__init__.py,sha256=v3e7coADLLuL6QbrMBCmLsYrXpwIZJ-3A_k4KhZ_NKM,293
2
+ panopticon/cli.py,sha256=NMUiyE6qEo_IgdwjzUMyI5tg_X-io0D8SIt4vJ3nkzw,5773
3
+ panopticon/memory.py,sha256=tsET5eL_V9sqN3PyNWT7kDiDB4sew-o8SXnipclFr-w,2292
4
+ panopticon/observer.py,sha256=cwhDuh_CpYaKHzPKhs16MFPvco1_QBooL5XZW3W4ohU,2625
5
+ panopticon/policies.py,sha256=-209r0LsG-6DAZRs4n2ZaX3ft0sFORuApLxYmkBkXd8,2204
6
+ panopticon/sentinel.py,sha256=qpb0Wuj3-oyafM_nDGMoR-4xQ2Hmdn4kh1csffP_Zow,5275
7
+ panopticon_cli-1.0.0.dist-info/licenses/LICENSE,sha256=RVE6q1ztFgMreAcgS3agYLyTymk3LfWEoCkYr4uQv64,1059
8
+ panopticon_cli-1.0.0.dist-info/METADATA,sha256=-Rvc6kvvzbUUsSHJtbeVilPyB3_tNz8yKQ1LhH75_J8,4096
9
+ panopticon_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ panopticon_cli-1.0.0.dist-info/entry_points.txt,sha256=gQ8sXaenMhuOvpNF7j1SZn4LyT8FovkoZtLNPYuF1MA,51
11
+ panopticon_cli-1.0.0.dist-info/top_level.txt,sha256=cwfjzY-pXdd-8YL4DE2KkDodExs8OXI_gUnFTJ8i1cQ,11
12
+ panopticon_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ panopticon = panopticon.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ panopticon