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.
Files changed (53) hide show
  1. clsplusplus/__init__.py +31 -0
  2. clsplusplus/api.py +1596 -0
  3. clsplusplus/auth.py +74 -0
  4. clsplusplus/cli.py +715 -0
  5. clsplusplus/client.py +462 -0
  6. clsplusplus/config.py +116 -0
  7. clsplusplus/cost_model.py +51 -0
  8. clsplusplus/demo_llm.py +133 -0
  9. clsplusplus/demo_llm_calls.py +100 -0
  10. clsplusplus/demo_local.py +515 -0
  11. clsplusplus/embeddings.py +52 -0
  12. clsplusplus/idempotency.py +66 -0
  13. clsplusplus/integration_service.py +256 -0
  14. clsplusplus/jwt_utils.py +39 -0
  15. clsplusplus/local_routes.py +781 -0
  16. clsplusplus/main.py +21 -0
  17. clsplusplus/memory_cycle.py +216 -0
  18. clsplusplus/memory_phase.py +3541 -0
  19. clsplusplus/memory_service.py +1323 -0
  20. clsplusplus/metrics.py +184 -0
  21. clsplusplus/middleware.py +325 -0
  22. clsplusplus/models.py +430 -0
  23. clsplusplus/permissions.py +54 -0
  24. clsplusplus/plasticity.py +148 -0
  25. clsplusplus/rate_limit.py +53 -0
  26. clsplusplus/rbac_service.py +86 -0
  27. clsplusplus/reconsolidation.py +71 -0
  28. clsplusplus/sleep_cycle.py +109 -0
  29. clsplusplus/stores/__init__.py +13 -0
  30. clsplusplus/stores/base.py +43 -0
  31. clsplusplus/stores/integration_store.py +648 -0
  32. clsplusplus/stores/l0_working_buffer.py +103 -0
  33. clsplusplus/stores/l1_indexing_store.py +427 -0
  34. clsplusplus/stores/l2_schema_graph.py +231 -0
  35. clsplusplus/stores/l3_deep_recess.py +182 -0
  36. clsplusplus/stores/l3_postgres.py +183 -0
  37. clsplusplus/stores/rbac_store.py +327 -0
  38. clsplusplus/stores/user_store.py +255 -0
  39. clsplusplus/stripe_service.py +136 -0
  40. clsplusplus/temporal.py +613 -0
  41. clsplusplus/test_suite.py +587 -0
  42. clsplusplus/tiers.py +109 -0
  43. clsplusplus/tracer.py +226 -0
  44. clsplusplus/usage.py +130 -0
  45. clsplusplus/user_embeddings.py +1636 -0
  46. clsplusplus/user_service.py +256 -0
  47. clsplusplus/webhook_dispatcher.py +229 -0
  48. clsplusplus-4.0.0.dist-info/METADATA +262 -0
  49. clsplusplus-4.0.0.dist-info/RECORD +53 -0
  50. clsplusplus-4.0.0.dist-info/WHEEL +5 -0
  51. clsplusplus-4.0.0.dist-info/entry_points.txt +2 -0
  52. clsplusplus-4.0.0.dist-info/licenses/LICENSE +201 -0
  53. 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()