clsplusplus 4.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.
- clsplusplus/__init__.py +31 -0
- clsplusplus/api.py +1596 -0
- clsplusplus/auth.py +74 -0
- clsplusplus/cli.py +715 -0
- clsplusplus/client.py +462 -0
- clsplusplus/config.py +116 -0
- clsplusplus/cost_model.py +51 -0
- clsplusplus/demo_llm.py +133 -0
- clsplusplus/demo_llm_calls.py +100 -0
- clsplusplus/demo_local.py +515 -0
- clsplusplus/embeddings.py +52 -0
- clsplusplus/idempotency.py +66 -0
- clsplusplus/integration_service.py +256 -0
- clsplusplus/jwt_utils.py +39 -0
- clsplusplus/local_routes.py +781 -0
- clsplusplus/main.py +21 -0
- clsplusplus/memory_cycle.py +216 -0
- clsplusplus/memory_phase.py +3541 -0
- clsplusplus/memory_service.py +1323 -0
- clsplusplus/metrics.py +184 -0
- clsplusplus/middleware.py +325 -0
- clsplusplus/models.py +430 -0
- clsplusplus/permissions.py +54 -0
- clsplusplus/plasticity.py +148 -0
- clsplusplus/rate_limit.py +53 -0
- clsplusplus/rbac_service.py +86 -0
- clsplusplus/reconsolidation.py +71 -0
- clsplusplus/sleep_cycle.py +109 -0
- clsplusplus/stores/__init__.py +13 -0
- clsplusplus/stores/base.py +43 -0
- clsplusplus/stores/integration_store.py +648 -0
- clsplusplus/stores/l0_working_buffer.py +103 -0
- clsplusplus/stores/l1_indexing_store.py +427 -0
- clsplusplus/stores/l2_schema_graph.py +231 -0
- clsplusplus/stores/l3_deep_recess.py +182 -0
- clsplusplus/stores/l3_postgres.py +183 -0
- clsplusplus/stores/rbac_store.py +327 -0
- clsplusplus/stores/user_store.py +255 -0
- clsplusplus/stripe_service.py +136 -0
- clsplusplus/temporal.py +613 -0
- clsplusplus/test_suite.py +587 -0
- clsplusplus/tiers.py +109 -0
- clsplusplus/tracer.py +226 -0
- clsplusplus/usage.py +130 -0
- clsplusplus/user_embeddings.py +1636 -0
- clsplusplus/user_service.py +256 -0
- clsplusplus/webhook_dispatcher.py +229 -0
- clsplusplus-4.0.0.dist-info/METADATA +262 -0
- clsplusplus-4.0.0.dist-info/RECORD +53 -0
- clsplusplus-4.0.0.dist-info/WHEEL +5 -0
- clsplusplus-4.0.0.dist-info/entry_points.txt +2 -0
- clsplusplus-4.0.0.dist-info/licenses/LICENSE +201 -0
- clsplusplus-4.0.0.dist-info/top_level.txt +1 -0
clsplusplus/cli.py
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
"""CLS++ CLI — Cross-model memory from your terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import configparser
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
CONFIG_PATH = Path.home() / ".clsplusplus"
|
|
13
|
+
DEFAULT_URL = "https://www.clsplusplus.com"
|
|
14
|
+
|
|
15
|
+
# Model routing
|
|
16
|
+
OPENAI_MODELS = {"gpt-4o", "gpt-4o-mini", "gpt-4", "gpt-4-turbo", "gpt-3.5-turbo", "o1", "o1-mini"}
|
|
17
|
+
ANTHROPIC_MODELS = {"claude", "claude-sonnet", "claude-haiku", "claude-opus",
|
|
18
|
+
"claude-sonnet-4-20250514", "claude-haiku-4-5-20251001"}
|
|
19
|
+
GEMINI_MODELS = {"gemini", "gemini-pro", "gemini-2.0-flash"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_config() -> dict:
|
|
23
|
+
"""Load config from ~/.clsplusplus."""
|
|
24
|
+
cfg = {
|
|
25
|
+
"api_key": os.environ.get("CLS_API_KEY", ""),
|
|
26
|
+
"user": os.environ.get("CLS_USER", os.environ.get("USER", "default")),
|
|
27
|
+
"url": os.environ.get("CLS_BASE_URL", DEFAULT_URL),
|
|
28
|
+
"openai_key": os.environ.get("OPENAI_API_KEY", ""),
|
|
29
|
+
"anthropic_key": os.environ.get("ANTHROPIC_API_KEY", ""),
|
|
30
|
+
"google_key": os.environ.get("GOOGLE_API_KEY", ""),
|
|
31
|
+
}
|
|
32
|
+
if CONFIG_PATH.exists():
|
|
33
|
+
cp = configparser.ConfigParser()
|
|
34
|
+
cp.read(CONFIG_PATH)
|
|
35
|
+
if cp.has_section("default"):
|
|
36
|
+
cfg["api_key"] = cp.get("default", "api_key", fallback=cfg["api_key"])
|
|
37
|
+
cfg["user"] = cp.get("default", "user", fallback=cfg["user"])
|
|
38
|
+
cfg["url"] = cp.get("default", "url", fallback=cfg["url"])
|
|
39
|
+
if cp.has_section("keys"):
|
|
40
|
+
cfg["openai_key"] = cp.get("keys", "openai", fallback=cfg["openai_key"])
|
|
41
|
+
cfg["anthropic_key"] = cp.get("keys", "anthropic", fallback=cfg["anthropic_key"])
|
|
42
|
+
cfg["google_key"] = cp.get("keys", "google", fallback=cfg["google_key"])
|
|
43
|
+
return cfg
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _save_config(cfg: dict) -> None:
|
|
47
|
+
"""Save config to ~/.clsplusplus."""
|
|
48
|
+
cp = configparser.ConfigParser()
|
|
49
|
+
cp["default"] = {
|
|
50
|
+
"api_key": cfg.get("api_key", ""),
|
|
51
|
+
"user": cfg.get("user", ""),
|
|
52
|
+
"url": cfg.get("url", DEFAULT_URL),
|
|
53
|
+
}
|
|
54
|
+
keys = {}
|
|
55
|
+
if cfg.get("openai_key"):
|
|
56
|
+
keys["openai"] = cfg["openai_key"]
|
|
57
|
+
if cfg.get("anthropic_key"):
|
|
58
|
+
keys["anthropic"] = cfg["anthropic_key"]
|
|
59
|
+
if cfg.get("google_key"):
|
|
60
|
+
keys["google"] = cfg["google_key"]
|
|
61
|
+
if keys:
|
|
62
|
+
cp["keys"] = keys
|
|
63
|
+
with open(CONFIG_PATH, "w") as f:
|
|
64
|
+
cp.write(f)
|
|
65
|
+
print(f"Config saved to {CONFIG_PATH}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _brain(cfg: dict):
|
|
69
|
+
"""Create a Brain instance from config."""
|
|
70
|
+
from clsplusplus import Brain
|
|
71
|
+
return Brain(user=cfg["user"], api_key=cfg["api_key"], url=cfg["url"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _resolve_model(model: str) -> str:
|
|
75
|
+
"""Resolve short model names to full IDs."""
|
|
76
|
+
aliases = {
|
|
77
|
+
"gpt4": "gpt-4o",
|
|
78
|
+
"gpt": "gpt-4o",
|
|
79
|
+
"openai": "gpt-4o",
|
|
80
|
+
"claude": "claude-sonnet-4-20250514",
|
|
81
|
+
"sonnet": "claude-sonnet-4-20250514",
|
|
82
|
+
"haiku": "claude-haiku-4-5-20251001",
|
|
83
|
+
"opus": "claude-opus-4-20250514",
|
|
84
|
+
"gemini": "gemini-2.0-flash",
|
|
85
|
+
}
|
|
86
|
+
return aliases.get(model.lower(), model)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _model_provider(model: str) -> str:
|
|
90
|
+
"""Determine provider from model name."""
|
|
91
|
+
m = model.lower()
|
|
92
|
+
if any(m.startswith(p) for p in ("gpt", "o1")):
|
|
93
|
+
return "openai"
|
|
94
|
+
if "claude" in m:
|
|
95
|
+
return "anthropic"
|
|
96
|
+
if "gemini" in m:
|
|
97
|
+
return "google"
|
|
98
|
+
return "openai" # default
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def cmd_init(args, cfg):
|
|
104
|
+
"""Interactive setup."""
|
|
105
|
+
print("CLS++ Setup")
|
|
106
|
+
print("=" * 40)
|
|
107
|
+
|
|
108
|
+
# API key
|
|
109
|
+
key = args.key or cfg["api_key"]
|
|
110
|
+
if not key:
|
|
111
|
+
key = input("CLS++ API key (from clsplusplus.com/profile.html#keys): ").strip()
|
|
112
|
+
if not key:
|
|
113
|
+
print("No API key provided. Get one at https://www.clsplusplus.com/profile.html#keys")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# User
|
|
117
|
+
user = args.user or cfg["user"]
|
|
118
|
+
if user == os.environ.get("USER", "default"):
|
|
119
|
+
entered = input(f"Username [{user}]: ").strip()
|
|
120
|
+
if entered:
|
|
121
|
+
user = entered
|
|
122
|
+
|
|
123
|
+
# LLM keys
|
|
124
|
+
openai_key = args.openai_key or cfg["openai_key"]
|
|
125
|
+
if not openai_key:
|
|
126
|
+
openai_key = input("OpenAI API key (optional, press Enter to skip): ").strip()
|
|
127
|
+
|
|
128
|
+
anthropic_key = args.anthropic_key or cfg["anthropic_key"]
|
|
129
|
+
if not anthropic_key:
|
|
130
|
+
anthropic_key = input("Anthropic API key (optional, press Enter to skip): ").strip()
|
|
131
|
+
|
|
132
|
+
google_key = args.google_key or cfg["google_key"]
|
|
133
|
+
if not google_key:
|
|
134
|
+
google_key = input("Google API key (optional, press Enter to skip): ").strip()
|
|
135
|
+
|
|
136
|
+
new_cfg = {
|
|
137
|
+
"api_key": key,
|
|
138
|
+
"user": user,
|
|
139
|
+
"url": args.url or cfg["url"],
|
|
140
|
+
"openai_key": openai_key,
|
|
141
|
+
"anthropic_key": anthropic_key,
|
|
142
|
+
"google_key": google_key,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Test connection
|
|
146
|
+
print("\nTesting connection...")
|
|
147
|
+
try:
|
|
148
|
+
import httpx
|
|
149
|
+
resp = httpx.get(f"{new_cfg['url'].rstrip('/')}/v1/health", timeout=10)
|
|
150
|
+
if resp.status_code == 200:
|
|
151
|
+
print(f"Connected to {new_cfg['url']}.")
|
|
152
|
+
else:
|
|
153
|
+
print(f"Server responded with {resp.status_code}. Check your URL.")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f"Warning: could not reach server ({e}). Config saved anyway.")
|
|
156
|
+
|
|
157
|
+
_save_config(new_cfg)
|
|
158
|
+
print("\nReady! Try: cls learn \"I prefer Python\"")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_chat(args, cfg):
|
|
162
|
+
"""Chat with any LLM — memories auto-captured and injected."""
|
|
163
|
+
import httpx
|
|
164
|
+
|
|
165
|
+
model = _resolve_model(args.model or "gpt-4o")
|
|
166
|
+
provider = _model_provider(model)
|
|
167
|
+
url = cfg["url"].rstrip("/")
|
|
168
|
+
|
|
169
|
+
# Determine LLM API key
|
|
170
|
+
if provider == "openai":
|
|
171
|
+
llm_key = cfg.get("openai_key", "")
|
|
172
|
+
elif provider == "anthropic":
|
|
173
|
+
llm_key = cfg.get("anthropic_key", "")
|
|
174
|
+
else:
|
|
175
|
+
llm_key = cfg.get("google_key", "")
|
|
176
|
+
|
|
177
|
+
if not llm_key:
|
|
178
|
+
print(f"No {provider} API key configured. Run: cls init")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
def send_message(message: str) -> str:
|
|
182
|
+
"""Send a message through CLS++ proxy and return response."""
|
|
183
|
+
headers = {"Content-Type": "application/json"}
|
|
184
|
+
|
|
185
|
+
if provider == "anthropic":
|
|
186
|
+
# Anthropic format
|
|
187
|
+
headers["x-api-key"] = llm_key
|
|
188
|
+
headers["anthropic-version"] = "2023-06-01"
|
|
189
|
+
if cfg["api_key"]:
|
|
190
|
+
headers["Authorization"] = f"Bearer {cfg['api_key']}"
|
|
191
|
+
body = {
|
|
192
|
+
"model": model,
|
|
193
|
+
"max_tokens": 1024,
|
|
194
|
+
"messages": [{"role": "user", "content": message}],
|
|
195
|
+
}
|
|
196
|
+
endpoint = f"{url}/v1/messages"
|
|
197
|
+
else:
|
|
198
|
+
# OpenAI format (also works for Gemini via compatible API)
|
|
199
|
+
headers["Authorization"] = f"Bearer {llm_key}"
|
|
200
|
+
body = {
|
|
201
|
+
"model": model,
|
|
202
|
+
"messages": [{"role": "user", "content": message}],
|
|
203
|
+
}
|
|
204
|
+
endpoint = f"{url}/v1/chat/completions"
|
|
205
|
+
|
|
206
|
+
# Add user namespace
|
|
207
|
+
body["user"] = cfg["user"]
|
|
208
|
+
|
|
209
|
+
resp = httpx.post(endpoint, json=body, headers=headers, timeout=90)
|
|
210
|
+
resp.raise_for_status()
|
|
211
|
+
data = resp.json()
|
|
212
|
+
|
|
213
|
+
if provider == "anthropic":
|
|
214
|
+
return data.get("content", [{}])[0].get("text", "")
|
|
215
|
+
else:
|
|
216
|
+
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
217
|
+
|
|
218
|
+
if args.message:
|
|
219
|
+
# One-shot mode
|
|
220
|
+
message = " ".join(args.message)
|
|
221
|
+
try:
|
|
222
|
+
reply = send_message(message)
|
|
223
|
+
print(f"\n{reply}\n")
|
|
224
|
+
except httpx.HTTPStatusError as e:
|
|
225
|
+
print(f"Error: {e.response.status_code} — {e.response.text[:200]}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
print(f"Error: {e}")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Interactive mode
|
|
231
|
+
print(f"CLS++ Chat (model: {model})")
|
|
232
|
+
print("Type /quit to exit, /memories to list, /who for profile\n")
|
|
233
|
+
|
|
234
|
+
while True:
|
|
235
|
+
try:
|
|
236
|
+
message = input("You: ").strip()
|
|
237
|
+
except (EOFError, KeyboardInterrupt):
|
|
238
|
+
print("\nGoodbye!")
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
if not message:
|
|
242
|
+
continue
|
|
243
|
+
if message == "/quit":
|
|
244
|
+
print("Goodbye!")
|
|
245
|
+
break
|
|
246
|
+
if message == "/memories":
|
|
247
|
+
brain = _brain(cfg)
|
|
248
|
+
for m in brain.all():
|
|
249
|
+
print(f" - {m}")
|
|
250
|
+
continue
|
|
251
|
+
if message == "/who":
|
|
252
|
+
brain = _brain(cfg)
|
|
253
|
+
profile = brain.who()
|
|
254
|
+
print(f"\n{profile.get('summary', 'No profile yet.')}\n")
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
reply = send_message(message)
|
|
259
|
+
print(f"\nAI: {reply}\n")
|
|
260
|
+
except httpx.HTTPStatusError as e:
|
|
261
|
+
print(f"Error: {e.response.status_code}")
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(f"Error: {e}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def cmd_learn(args, cfg):
|
|
267
|
+
"""Store a memory."""
|
|
268
|
+
brain = _brain(cfg)
|
|
269
|
+
fact = " ".join(args.fact)
|
|
270
|
+
mid = brain.learn(fact)
|
|
271
|
+
print(f"Learned: \"{fact}\"")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def cmd_ask(args, cfg):
|
|
275
|
+
"""Query memories."""
|
|
276
|
+
brain = _brain(cfg)
|
|
277
|
+
question = " ".join(args.question)
|
|
278
|
+
results = brain.ask(question, limit=args.limit)
|
|
279
|
+
if not results:
|
|
280
|
+
print("No matching memories found.")
|
|
281
|
+
return
|
|
282
|
+
for r in results:
|
|
283
|
+
print(f" - {r}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def cmd_memories(args, cfg):
|
|
287
|
+
"""List all stored memories."""
|
|
288
|
+
brain = _brain(cfg)
|
|
289
|
+
mems = brain.all(limit=args.limit)
|
|
290
|
+
if not mems:
|
|
291
|
+
print("No memories stored yet. Try: cls learn \"I prefer Python\"")
|
|
292
|
+
return
|
|
293
|
+
print(f"{len(mems)} memories:\n")
|
|
294
|
+
for i, m in enumerate(mems, 1):
|
|
295
|
+
print(f" {i}. {m}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def cmd_who(args, cfg):
|
|
299
|
+
"""Show auto-generated user profile."""
|
|
300
|
+
brain = _brain(cfg)
|
|
301
|
+
profile = brain.who()
|
|
302
|
+
print(f"\nProfile: {profile.get('user', cfg['user'])}")
|
|
303
|
+
print(f"Memories: {profile.get('count', 0)}\n")
|
|
304
|
+
if profile.get("summary"):
|
|
305
|
+
print(profile["summary"])
|
|
306
|
+
print()
|
|
307
|
+
for fact in profile.get("facts", []):
|
|
308
|
+
print(f" - {fact}")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def cmd_status(args, cfg):
|
|
312
|
+
"""Show connection status and memory count."""
|
|
313
|
+
import httpx
|
|
314
|
+
|
|
315
|
+
url = cfg["url"].rstrip("/")
|
|
316
|
+
print(f"Server: {url}")
|
|
317
|
+
print(f"User: {cfg['user']}")
|
|
318
|
+
print(f"API Key: {'configured' if cfg['api_key'] else 'not set'}")
|
|
319
|
+
print(f"Config: {CONFIG_PATH}")
|
|
320
|
+
|
|
321
|
+
# Health check
|
|
322
|
+
try:
|
|
323
|
+
resp = httpx.get(f"{url}/v1/health", timeout=10)
|
|
324
|
+
data = resp.json()
|
|
325
|
+
print(f"Status: {data.get('status', 'unknown')}")
|
|
326
|
+
except Exception:
|
|
327
|
+
print("Status: unreachable")
|
|
328
|
+
|
|
329
|
+
# Memory count
|
|
330
|
+
if cfg["api_key"]:
|
|
331
|
+
try:
|
|
332
|
+
brain = _brain(cfg)
|
|
333
|
+
print(f"Memories: {brain.count()}")
|
|
334
|
+
except Exception:
|
|
335
|
+
print("Memories: unable to fetch")
|
|
336
|
+
|
|
337
|
+
# LLM keys
|
|
338
|
+
print(f"\nOpenAI: {'configured' if cfg.get('openai_key') else 'not set'}")
|
|
339
|
+
print(f"Anthropic: {'configured' if cfg.get('anthropic_key') else 'not set'}")
|
|
340
|
+
print(f"Google: {'configured' if cfg.get('google_key') else 'not set'}")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def cmd_forget(args, cfg):
|
|
344
|
+
"""Delete a memory."""
|
|
345
|
+
brain = _brain(cfg)
|
|
346
|
+
fact = " ".join(args.fact)
|
|
347
|
+
ok = brain.forget(fact)
|
|
348
|
+
if ok:
|
|
349
|
+
print(f"Forgotten: \"{fact}\"")
|
|
350
|
+
else:
|
|
351
|
+
print(f"Could not find memory: \"{fact}\"")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def cmd_absorb(args, cfg):
|
|
355
|
+
"""Bulk-learn from a file."""
|
|
356
|
+
brain = _brain(cfg)
|
|
357
|
+
path = Path(args.file)
|
|
358
|
+
if not path.exists():
|
|
359
|
+
print(f"File not found: {path}")
|
|
360
|
+
return
|
|
361
|
+
content = path.read_text()
|
|
362
|
+
count = brain.absorb(content, source=path.name)
|
|
363
|
+
print(f"Absorbed {count} facts from {path.name}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def cmd_context(args, cfg):
|
|
367
|
+
"""Get LLM-ready context string."""
|
|
368
|
+
brain = _brain(cfg)
|
|
369
|
+
topic = " ".join(args.topic) if args.topic else ""
|
|
370
|
+
ctx = brain.context(topic)
|
|
371
|
+
if ctx:
|
|
372
|
+
print(ctx)
|
|
373
|
+
else:
|
|
374
|
+
print("No relevant context found.")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def cmd_models(args, cfg):
|
|
378
|
+
"""List all supported models."""
|
|
379
|
+
print("SUPPORTED MODELS")
|
|
380
|
+
print("=" * 60)
|
|
381
|
+
print()
|
|
382
|
+
print(" OPENAI (requires OPENAI_API_KEY)")
|
|
383
|
+
print(" ---------------------------------")
|
|
384
|
+
print(" gpt-4o GPT-4o (recommended)")
|
|
385
|
+
print(" gpt-4o-mini GPT-4o Mini (fast, cheap)")
|
|
386
|
+
print(" gpt-4 GPT-4")
|
|
387
|
+
print(" gpt-4-turbo GPT-4 Turbo")
|
|
388
|
+
print(" gpt-3.5-turbo GPT-3.5 Turbo (legacy)")
|
|
389
|
+
print(" o1 OpenAI o1")
|
|
390
|
+
print(" o1-mini OpenAI o1 Mini")
|
|
391
|
+
print()
|
|
392
|
+
print(" ANTHROPIC (requires ANTHROPIC_API_KEY)")
|
|
393
|
+
print(" --------------------------------------")
|
|
394
|
+
print(" claude Claude Sonnet (default)")
|
|
395
|
+
print(" claude-sonnet Claude Sonnet 4")
|
|
396
|
+
print(" claude-haiku Claude Haiku 4.5 (fast)")
|
|
397
|
+
print(" claude-opus Claude Opus 4")
|
|
398
|
+
print()
|
|
399
|
+
print(" GOOGLE (requires GOOGLE_API_KEY)")
|
|
400
|
+
print(" --------------------------------")
|
|
401
|
+
print(" gemini Gemini 2.0 Flash (default)")
|
|
402
|
+
print(" gemini-pro Gemini Pro")
|
|
403
|
+
print()
|
|
404
|
+
print(" SHORTCUTS")
|
|
405
|
+
print(" ---------")
|
|
406
|
+
print(" gpt -> gpt-4o")
|
|
407
|
+
print(" sonnet -> claude-sonnet-4-20250514")
|
|
408
|
+
print(" haiku -> claude-haiku-4-5-20251001")
|
|
409
|
+
print(" opus -> claude-opus-4-20250514")
|
|
410
|
+
print()
|
|
411
|
+
print(" EXAMPLES")
|
|
412
|
+
print(" --------")
|
|
413
|
+
print(' cls chat -m gpt-4o "What is memory?"')
|
|
414
|
+
print(' cls chat -m claude "What did I just ask GPT?"')
|
|
415
|
+
print(' cls chat -m gemini "Summarize what you know about me"')
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
MAIN_HELP = """\
|
|
421
|
+
NAME
|
|
422
|
+
cls - CLS++ persistent memory for every AI model
|
|
423
|
+
|
|
424
|
+
SYNOPSIS
|
|
425
|
+
cls <command> [options] [arguments]
|
|
426
|
+
|
|
427
|
+
DESCRIPTION
|
|
428
|
+
CLS++ gives every AI model persistent memory. Tell something to GPT-4,
|
|
429
|
+
switch to Claude, it already knows. Memories persist across sessions,
|
|
430
|
+
models, and devices.
|
|
431
|
+
|
|
432
|
+
COMMANDS
|
|
433
|
+
init Setup API keys and configuration
|
|
434
|
+
chat Chat with any LLM (memories auto-captured and injected)
|
|
435
|
+
learn Store a memory
|
|
436
|
+
ask Query memories semantically
|
|
437
|
+
memories List all stored memories
|
|
438
|
+
models List all supported models and shortcuts
|
|
439
|
+
who Show auto-generated user profile
|
|
440
|
+
status Show connection status, memory count, configured keys
|
|
441
|
+
forget Delete a memory
|
|
442
|
+
absorb Bulk-learn from a file
|
|
443
|
+
context Get LLM-ready context string for prompts
|
|
444
|
+
|
|
445
|
+
QUICK START
|
|
446
|
+
cls init --key YOUR_API_KEY
|
|
447
|
+
cls learn "I prefer Python and dark mode"
|
|
448
|
+
cls ask "What are my preferences?"
|
|
449
|
+
|
|
450
|
+
CROSS-MODEL MEMORY TRANSFER
|
|
451
|
+
cls chat -m gpt-4o "My name is Raj and I love Python"
|
|
452
|
+
cls chat -m claude "What's my name and what do I love?"
|
|
453
|
+
# -> Claude knows: "Your name is Raj and you love Python."
|
|
454
|
+
|
|
455
|
+
CONFIGURATION
|
|
456
|
+
Config file: ~/.clsplusplus
|
|
457
|
+
Environment variables: CLS_API_KEY, CLS_BASE_URL, CLS_USER,
|
|
458
|
+
OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY
|
|
459
|
+
|
|
460
|
+
SEE ALSO
|
|
461
|
+
cls <command> --help Detailed help for a specific command
|
|
462
|
+
cls models List all supported models
|
|
463
|
+
https://www.clsplusplus.com/integrate.html
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def main():
|
|
468
|
+
parser = argparse.ArgumentParser(
|
|
469
|
+
prog="cls",
|
|
470
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
471
|
+
description=MAIN_HELP,
|
|
472
|
+
)
|
|
473
|
+
parser.add_argument("--user", help="User identifier (overrides config)")
|
|
474
|
+
parser.add_argument("--key", help="CLS++ API key (overrides config)")
|
|
475
|
+
parser.add_argument("--url", help="Server URL (overrides config)")
|
|
476
|
+
|
|
477
|
+
sub = parser.add_subparsers(dest="command")
|
|
478
|
+
|
|
479
|
+
# init
|
|
480
|
+
p_init = sub.add_parser("init", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
481
|
+
help="Setup API keys and configuration",
|
|
482
|
+
description="""\
|
|
483
|
+
NAME
|
|
484
|
+
cls init - Configure CLS++ API keys and connection
|
|
485
|
+
|
|
486
|
+
DESCRIPTION
|
|
487
|
+
Interactive setup wizard. Saves configuration to ~/.clsplusplus.
|
|
488
|
+
After init, all other commands work without passing keys.
|
|
489
|
+
|
|
490
|
+
EXAMPLES
|
|
491
|
+
cls init
|
|
492
|
+
cls init --key cls_live_xxx
|
|
493
|
+
cls init --key cls_live_xxx --openai-key sk-xxx --user raj
|
|
494
|
+
""")
|
|
495
|
+
p_init.add_argument("--key", dest="key", help="CLS++ API key")
|
|
496
|
+
p_init.add_argument("--user", help="Username")
|
|
497
|
+
p_init.add_argument("--url", help="Server URL")
|
|
498
|
+
p_init.add_argument("--openai-key", dest="openai_key", help="OpenAI API key")
|
|
499
|
+
p_init.add_argument("--anthropic-key", dest="anthropic_key", help="Anthropic API key")
|
|
500
|
+
p_init.add_argument("--google-key", dest="google_key", help="Google API key")
|
|
501
|
+
|
|
502
|
+
# chat
|
|
503
|
+
p_chat = sub.add_parser("chat", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
504
|
+
help="Chat with any LLM — memories transfer across models",
|
|
505
|
+
description="""\
|
|
506
|
+
NAME
|
|
507
|
+
cls chat - Chat with any LLM with persistent cross-model memory
|
|
508
|
+
|
|
509
|
+
DESCRIPTION
|
|
510
|
+
Send a message to any supported model. CLS++ automatically:
|
|
511
|
+
1. Retrieves relevant memories and injects them into the prompt
|
|
512
|
+
2. Forwards the request to the LLM (OpenAI, Anthropic, Google)
|
|
513
|
+
3. Captures the conversation as new memories
|
|
514
|
+
4. Returns the LLM's response
|
|
515
|
+
|
|
516
|
+
Memories persist across models. Tell GPT-4 your name, then ask
|
|
517
|
+
Claude — it already knows.
|
|
518
|
+
|
|
519
|
+
Omit the message to enter interactive mode.
|
|
520
|
+
|
|
521
|
+
Run 'cls models' to see all supported models.
|
|
522
|
+
|
|
523
|
+
EXAMPLES
|
|
524
|
+
cls chat -m gpt-4o "My name is Raj and I love Python"
|
|
525
|
+
cls chat -m claude "What's my name?"
|
|
526
|
+
cls chat -m gemini "Summarize what you know about me"
|
|
527
|
+
cls chat # interactive mode
|
|
528
|
+
cls chat -m haiku "Quick question" # use shortcut names
|
|
529
|
+
""")
|
|
530
|
+
p_chat.add_argument("--model", "-m", default="gpt-4o",
|
|
531
|
+
help="Model name or shortcut (default: gpt-4o). Run 'cls models' for full list")
|
|
532
|
+
p_chat.add_argument("message", nargs="*", help="Message (omit for interactive mode)")
|
|
533
|
+
|
|
534
|
+
# learn
|
|
535
|
+
p_learn = sub.add_parser("learn", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
536
|
+
help="Store a memory",
|
|
537
|
+
description="""\
|
|
538
|
+
NAME
|
|
539
|
+
cls learn - Teach CLS++ a fact to remember
|
|
540
|
+
|
|
541
|
+
DESCRIPTION
|
|
542
|
+
Store any fact as a persistent memory. The memory is available
|
|
543
|
+
to all models and persists across sessions.
|
|
544
|
+
|
|
545
|
+
EXAMPLES
|
|
546
|
+
cls learn "I prefer Python and dark mode"
|
|
547
|
+
cls learn "My project is called Atlas and uses FastAPI"
|
|
548
|
+
cls learn "Meeting with Alice on Friday at 3pm"
|
|
549
|
+
""")
|
|
550
|
+
p_learn.add_argument("fact", nargs="+", help="Fact to remember")
|
|
551
|
+
|
|
552
|
+
# ask
|
|
553
|
+
p_ask = sub.add_parser("ask", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
554
|
+
help="Query memories semantically",
|
|
555
|
+
description="""\
|
|
556
|
+
NAME
|
|
557
|
+
cls ask - Query memories using natural language
|
|
558
|
+
|
|
559
|
+
DESCRIPTION
|
|
560
|
+
Semantic search across all stored memories. Returns the most
|
|
561
|
+
relevant matches ranked by confidence.
|
|
562
|
+
|
|
563
|
+
EXAMPLES
|
|
564
|
+
cls ask "What are my coding preferences?"
|
|
565
|
+
cls ask "What meetings do I have?" -n 10
|
|
566
|
+
cls ask "Tell me about my project"
|
|
567
|
+
""")
|
|
568
|
+
p_ask.add_argument("question", nargs="+", help="Question to ask")
|
|
569
|
+
p_ask.add_argument("--limit", "-n", type=int, default=5, help="Max results (default: 5)")
|
|
570
|
+
|
|
571
|
+
# memories
|
|
572
|
+
p_mems = sub.add_parser("memories", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
573
|
+
help="List all stored memories",
|
|
574
|
+
description="""\
|
|
575
|
+
NAME
|
|
576
|
+
cls memories - List all stored memories
|
|
577
|
+
|
|
578
|
+
EXAMPLES
|
|
579
|
+
cls memories
|
|
580
|
+
cls memories -n 100
|
|
581
|
+
""")
|
|
582
|
+
p_mems.add_argument("--limit", "-n", type=int, default=50, help="Max results (default: 50)")
|
|
583
|
+
|
|
584
|
+
# models
|
|
585
|
+
sub.add_parser("models", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
586
|
+
help="List all supported models and shortcuts",
|
|
587
|
+
description="""\
|
|
588
|
+
NAME
|
|
589
|
+
cls models - Show all supported LLM models
|
|
590
|
+
|
|
591
|
+
DESCRIPTION
|
|
592
|
+
Lists every model CLS++ can route to, organized by provider,
|
|
593
|
+
with shortcut names for convenience.
|
|
594
|
+
""")
|
|
595
|
+
|
|
596
|
+
# who
|
|
597
|
+
sub.add_parser("who", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
598
|
+
help="Show auto-generated user profile",
|
|
599
|
+
description="""\
|
|
600
|
+
NAME
|
|
601
|
+
cls who - Show auto-generated user profile
|
|
602
|
+
|
|
603
|
+
DESCRIPTION
|
|
604
|
+
Generates a structured profile from all stored memories.
|
|
605
|
+
Shows facts, summary, and memory count.
|
|
606
|
+
|
|
607
|
+
EXAMPLES
|
|
608
|
+
cls who
|
|
609
|
+
""")
|
|
610
|
+
|
|
611
|
+
# status
|
|
612
|
+
sub.add_parser("status", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
613
|
+
help="Show connection status, memory count, configured keys",
|
|
614
|
+
description="""\
|
|
615
|
+
NAME
|
|
616
|
+
cls status - Show connection and configuration status
|
|
617
|
+
|
|
618
|
+
DESCRIPTION
|
|
619
|
+
Displays server URL, user, API key status, health check result,
|
|
620
|
+
memory count, and which LLM provider keys are configured.
|
|
621
|
+
|
|
622
|
+
EXAMPLES
|
|
623
|
+
cls status
|
|
624
|
+
""")
|
|
625
|
+
|
|
626
|
+
# forget
|
|
627
|
+
p_forget = sub.add_parser("forget", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
628
|
+
help="Delete a memory",
|
|
629
|
+
description="""\
|
|
630
|
+
NAME
|
|
631
|
+
cls forget - Delete a memory
|
|
632
|
+
|
|
633
|
+
DESCRIPTION
|
|
634
|
+
Remove a specific memory by its text or ID. Supports GDPR
|
|
635
|
+
right-to-be-forgotten compliance.
|
|
636
|
+
|
|
637
|
+
EXAMPLES
|
|
638
|
+
cls forget "I work at Google"
|
|
639
|
+
cls forget "old-memory-id-here"
|
|
640
|
+
""")
|
|
641
|
+
p_forget.add_argument("fact", nargs="+", help="Fact text or memory ID to forget")
|
|
642
|
+
|
|
643
|
+
# absorb
|
|
644
|
+
p_absorb = sub.add_parser("absorb", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
645
|
+
help="Bulk-learn from a file",
|
|
646
|
+
description="""\
|
|
647
|
+
NAME
|
|
648
|
+
cls absorb - Bulk-learn facts from a text file
|
|
649
|
+
|
|
650
|
+
DESCRIPTION
|
|
651
|
+
Reads a file, splits into sentences/paragraphs, and stores
|
|
652
|
+
each as a separate memory. Good for importing notes, documents,
|
|
653
|
+
or conversation logs.
|
|
654
|
+
|
|
655
|
+
EXAMPLES
|
|
656
|
+
cls absorb meeting-notes.txt
|
|
657
|
+
cls absorb ~/Documents/project-brief.md
|
|
658
|
+
""")
|
|
659
|
+
p_absorb.add_argument("file", help="Path to text file")
|
|
660
|
+
|
|
661
|
+
# context
|
|
662
|
+
p_context = sub.add_parser("context", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
663
|
+
help="Get LLM-ready context string for prompts",
|
|
664
|
+
description="""\
|
|
665
|
+
NAME
|
|
666
|
+
cls context - Generate context string for LLM prompts
|
|
667
|
+
|
|
668
|
+
DESCRIPTION
|
|
669
|
+
Returns a formatted block of relevant memories suitable for
|
|
670
|
+
injecting into a system prompt. Use this when building custom
|
|
671
|
+
LLM integrations.
|
|
672
|
+
|
|
673
|
+
EXAMPLES
|
|
674
|
+
cls context "coding help"
|
|
675
|
+
cls context "meeting preparation"
|
|
676
|
+
""")
|
|
677
|
+
p_context.add_argument("topic", nargs="*", help="Topic to get context for")
|
|
678
|
+
|
|
679
|
+
args = parser.parse_args()
|
|
680
|
+
if not args.command:
|
|
681
|
+
parser.print_help()
|
|
682
|
+
return
|
|
683
|
+
|
|
684
|
+
# Load config, apply CLI overrides
|
|
685
|
+
cfg = _load_config()
|
|
686
|
+
if getattr(args, "key", None) and args.command != "init":
|
|
687
|
+
cfg["api_key"] = args.key
|
|
688
|
+
if getattr(args, "user", None) and args.command != "init":
|
|
689
|
+
cfg["user"] = args.user
|
|
690
|
+
if getattr(args, "url", None) and args.command != "init":
|
|
691
|
+
cfg["url"] = args.url
|
|
692
|
+
|
|
693
|
+
commands = {
|
|
694
|
+
"init": cmd_init,
|
|
695
|
+
"chat": cmd_chat,
|
|
696
|
+
"learn": cmd_learn,
|
|
697
|
+
"ask": cmd_ask,
|
|
698
|
+
"memories": cmd_memories,
|
|
699
|
+
"models": cmd_models,
|
|
700
|
+
"who": cmd_who,
|
|
701
|
+
"status": cmd_status,
|
|
702
|
+
"forget": cmd_forget,
|
|
703
|
+
"absorb": cmd_absorb,
|
|
704
|
+
"context": cmd_context,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn = commands.get(args.command)
|
|
708
|
+
if fn:
|
|
709
|
+
fn(args, cfg)
|
|
710
|
+
else:
|
|
711
|
+
parser.print_help()
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
if __name__ == "__main__":
|
|
715
|
+
main()
|