comptext-codex 5.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.
@@ -0,0 +1,236 @@
1
+ """Module & command registry with decorator-based auto-registration.
2
+
3
+ Instead of maintaining separate YAML files and manually wiring modules,
4
+ every module class uses ``@codex_module`` and every handler uses
5
+ ``@codex_command`` to self-register at import time.
6
+
7
+ Usage::
8
+
9
+ from comptext_codex.registry import codex_module, codex_command
10
+
11
+ @codex_module(code="A", name="Core Commands",
12
+ purpose="Essential text manipulation commands")
13
+ class ModuleA:
14
+ @codex_command(syntax="@A:compress <text>",
15
+ description="Compress text by removing redundancy",
16
+ aliases=["shrink", "condense"],
17
+ token_cost_hint=50)
18
+ def execute_compress(self, text, *, context=None, **kw):
19
+ ...
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from dataclasses import dataclass, field
26
+ from typing import Any, Callable, Dict, List, Optional, Type
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Data classes
33
+ # ---------------------------------------------------------------------------
34
+
35
+ @dataclass
36
+ class CommandMeta:
37
+ """Metadata attached to a single command handler."""
38
+ command: str # derived from method name (execute_<command>)
39
+ module: str = "" # filled in by @codex_module
40
+ syntax: str = ""
41
+ description: str = ""
42
+ aliases: List[str] = field(default_factory=list)
43
+ examples: List[str] = field(default_factory=list)
44
+ token_cost_hint: int = 0
45
+ mcp_exposed: bool = True
46
+
47
+ def to_dict(self) -> Dict[str, Any]:
48
+ return {
49
+ "module": self.module,
50
+ "command": self.command,
51
+ "syntax": self.syntax,
52
+ "description": self.description,
53
+ "aliases": self.aliases,
54
+ "examples": self.examples,
55
+ "token_cost_hint": self.token_cost_hint,
56
+ "mcp_exposed": self.mcp_exposed,
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class ModuleMeta:
62
+ """Metadata attached to a module class."""
63
+ code: str
64
+ name: str
65
+ purpose: str = ""
66
+ token_priority: str = "medium"
67
+ mcp_exposed: bool = True
68
+ security: Dict[str, Any] = field(default_factory=dict)
69
+ privacy: Dict[str, Any] = field(default_factory=dict)
70
+
71
+ def to_dict(self) -> Dict[str, Any]:
72
+ return {
73
+ "code": self.code,
74
+ "name": self.name,
75
+ "purpose": self.purpose,
76
+ "token_priority": self.token_priority,
77
+ "mcp_exposed": self.mcp_exposed,
78
+ "security": self.security,
79
+ "privacy": self.privacy,
80
+ }
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Global registry (populated at import time)
85
+ # ---------------------------------------------------------------------------
86
+
87
+ class ModuleRegistry:
88
+ """Central registry holding all discovered module classes and their commands."""
89
+
90
+ def __init__(self):
91
+ self._modules: Dict[str, Type] = {} # code -> class
92
+ self._module_meta: Dict[str, ModuleMeta] = {}
93
+ self._instances: Dict[str, Any] = {} # lazy singleton per code
94
+
95
+ def register_module(self, cls: Type, meta: ModuleMeta) -> None:
96
+ self._modules[meta.code] = cls
97
+ self._module_meta[meta.code] = meta
98
+ logger.debug("Registered module %s (%s)", meta.code, meta.name)
99
+
100
+ def get_instance(self, code: str) -> Optional[Any]:
101
+ if code not in self._modules:
102
+ return None
103
+ if code not in self._instances:
104
+ self._instances[code] = self._modules[code]()
105
+ return self._instances[code]
106
+
107
+ def list_module_codes(self) -> List[str]:
108
+ return sorted(self._modules.keys())
109
+
110
+ def get_module_meta(self, code: str) -> Optional[ModuleMeta]:
111
+ return self._module_meta.get(code)
112
+
113
+ def all_module_meta(self) -> List[ModuleMeta]:
114
+ return [self._module_meta[c] for c in sorted(self._module_meta)]
115
+
116
+ def all_command_meta(self) -> List[CommandMeta]:
117
+ """Collect CommandMeta from all registered modules."""
118
+ result: List[CommandMeta] = []
119
+ for code in sorted(self._modules):
120
+ cls = self._modules[code]
121
+ for attr in dir(cls):
122
+ fn = getattr(cls, attr, None)
123
+ cm: Optional[CommandMeta] = getattr(fn, "_command_meta", None)
124
+ if cm is not None:
125
+ result.append(cm)
126
+ return result
127
+
128
+ def get_commands_for_module(self, code: str) -> List[CommandMeta]:
129
+ cls = self._modules.get(code)
130
+ if cls is None:
131
+ return []
132
+ result: List[CommandMeta] = []
133
+ for attr in dir(cls):
134
+ fn = getattr(cls, attr, None)
135
+ cm: Optional[CommandMeta] = getattr(fn, "_command_meta", None)
136
+ if cm is not None and cm.module == code:
137
+ result.append(cm)
138
+ return result
139
+
140
+ def clear(self):
141
+ """Reset for testing."""
142
+ self._modules.clear()
143
+ self._module_meta.clear()
144
+ self._instances.clear()
145
+
146
+
147
+ # Singleton instance
148
+ registry = ModuleRegistry()
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Decorators
153
+ # ---------------------------------------------------------------------------
154
+
155
+ def codex_module(
156
+ code: str,
157
+ name: str,
158
+ purpose: str = "",
159
+ token_priority: str = "medium",
160
+ mcp_exposed: bool = True,
161
+ security: Optional[Dict[str, Any]] = None,
162
+ privacy: Optional[Dict[str, Any]] = None,
163
+ ):
164
+ """Class decorator that registers a module in the global registry.
165
+
166
+ Also walks the class body and stamps ``module=code`` on every
167
+ ``@codex_command``-decorated method.
168
+ """
169
+ def decorator(cls: Type) -> Type:
170
+ meta = ModuleMeta(
171
+ code=code,
172
+ name=name,
173
+ purpose=purpose,
174
+ token_priority=token_priority,
175
+ mcp_exposed=mcp_exposed,
176
+ security=security or {},
177
+ privacy=privacy or {},
178
+ )
179
+ cls._module_meta = meta # type: ignore[attr-defined]
180
+
181
+ # Patch module code into every command handler defined on this class
182
+ for attr_name in list(vars(cls)):
183
+ fn = getattr(cls, attr_name, None)
184
+ cm: Optional[CommandMeta] = getattr(fn, "_command_meta", None)
185
+ if cm is not None:
186
+ cm.module = code
187
+
188
+ registry.register_module(cls, meta)
189
+ return cls
190
+
191
+ return decorator
192
+
193
+
194
+ def codex_command(
195
+ syntax: str = "",
196
+ description: str = "",
197
+ aliases: Optional[List[str]] = None,
198
+ examples: Optional[List[str]] = None,
199
+ token_cost_hint: int = 0,
200
+ mcp_exposed: bool = True,
201
+ ):
202
+ """Method decorator that attaches command metadata to ``execute_*`` methods.
203
+
204
+ The *command* name is derived from the method name by stripping the
205
+ ``execute_`` prefix.
206
+ """
207
+ def decorator(fn: Callable) -> Callable:
208
+ name = fn.__name__
209
+ if name.startswith("execute_"):
210
+ cmd_name = name[8:] # strip "execute_"
211
+ else:
212
+ cmd_name = name
213
+
214
+ fn._command_meta = CommandMeta( # type: ignore[attr-defined]
215
+ command=cmd_name,
216
+ syntax=syntax,
217
+ description=description,
218
+ aliases=aliases or [],
219
+ examples=examples or [],
220
+ token_cost_hint=token_cost_hint,
221
+ mcp_exposed=mcp_exposed,
222
+ )
223
+ return fn
224
+
225
+ return decorator
226
+
227
+
228
+ def ensure_modules_loaded() -> None:
229
+ """Import all module files so decorators run and modules register."""
230
+ import importlib
231
+
232
+ for letter in "abcdefghijklm":
233
+ try:
234
+ importlib.import_module(f"comptext_codex.modules.module_{letter}")
235
+ except ImportError:
236
+ pass
comptext_codex/repl.py ADDED
@@ -0,0 +1,410 @@
1
+ """Interactive REPL for CompText Codex.
2
+
3
+ Provides an interactive shell with:
4
+ - Command history and editing
5
+ - Tab completion for modules and commands
6
+ - Syntax highlighting
7
+ - Multi-line input support
8
+ - Built-in help system
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import List, Optional, Dict, Any
17
+
18
+ try:
19
+ import readline
20
+ READLINE_AVAILABLE = True
21
+ except ImportError:
22
+ READLINE_AVAILABLE = False
23
+
24
+ from .parser import CompTextParser, CompTextCommand
25
+ from .executor import CompTextExecutor, ExecutionResult
26
+
27
+
28
+ class CompTextREPL:
29
+ """Interactive REPL for CompText DSL."""
30
+
31
+ def __init__(self, codex_dir: Optional[str] = None):
32
+ """Initialize REPL.
33
+
34
+ Args:
35
+ codex_dir: Path to codex directory (defaults to ./codex)
36
+ """
37
+ self.codex_dir = codex_dir or "codex"
38
+ self.parser = CompTextParser(codex_dir=self.codex_dir)
39
+ self.executor = CompTextExecutor(codex_dir=self.codex_dir)
40
+ self.context: Dict[str, Any] = {}
41
+ self.history: List[str] = []
42
+ self.multiline_buffer: List[str] = []
43
+
44
+ # Available modules and commands for autocomplete
45
+ self.modules = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M']
46
+ self.commands_by_module = self._load_available_commands()
47
+
48
+ # Setup readline if available
49
+ if READLINE_AVAILABLE:
50
+ self._setup_readline()
51
+
52
+ def _load_available_commands(self) -> Dict[str, List[str]]:
53
+ """Load available commands from executor."""
54
+ commands_by_module = {}
55
+ available = self.executor.get_available_commands()
56
+
57
+ for cmd_info in available:
58
+ module = cmd_info.get('module', 'A')
59
+ command = cmd_info.get('command', '')
60
+
61
+ if module not in commands_by_module:
62
+ commands_by_module[module] = []
63
+
64
+ if command and command not in commands_by_module[module]:
65
+ commands_by_module[module].append(command)
66
+
67
+ return commands_by_module
68
+
69
+ def _setup_readline(self):
70
+ """Setup readline for history and completion."""
71
+ # Enable tab completion
72
+ readline.parse_and_bind('tab: complete')
73
+ readline.set_completer(self._completer)
74
+
75
+ # Load history file
76
+ history_file = Path.home() / '.comptext_history'
77
+ if history_file.exists():
78
+ try:
79
+ readline.read_history_file(history_file)
80
+ except Exception:
81
+ pass
82
+
83
+ # Save history on exit
84
+ import atexit
85
+ atexit.register(lambda: self._save_history(history_file))
86
+
87
+ def _save_history(self, history_file: Path):
88
+ """Save command history to file."""
89
+ try:
90
+ readline.write_history_file(history_file)
91
+ except Exception:
92
+ pass
93
+
94
+ def _completer(self, text: str, state: int) -> Optional[str]:
95
+ """Readline completer for tab completion."""
96
+ options = []
97
+
98
+ # Complete module codes
99
+ if text.startswith('@') and ':' not in text and '[' not in text:
100
+ module_prefix = text[1:]
101
+ options = [f"@{m}:" for m in self.modules if m.startswith(module_prefix.upper())]
102
+
103
+ # Complete commands after module:
104
+ elif ':' in text:
105
+ parts = text.split(':')
106
+ if len(parts) == 2:
107
+ module = parts[0].strip('@')
108
+ cmd_prefix = parts[1]
109
+
110
+ if module in self.commands_by_module:
111
+ options = [
112
+ f"@{module}:{cmd}"
113
+ for cmd in self.commands_by_module[module]
114
+ if cmd.startswith(cmd_prefix.lower())
115
+ ]
116
+
117
+ # Complete REPL commands
118
+ elif text.startswith('.'):
119
+ repl_commands = ['.help', '.commands', '.context', '.clear', '.history', '.exit', '.quit']
120
+ options = [cmd for cmd in repl_commands if cmd.startswith(text)]
121
+
122
+ if state < len(options):
123
+ return options[state]
124
+ return None
125
+
126
+ def print_banner(self):
127
+ """Print welcome banner."""
128
+ banner = """
129
+ ╔══════════════════════════════════════════════════════════════╗
130
+ ║ CompText Codex Interactive REPL v3.5.0 ║
131
+ ║ ║
132
+ ║ Efficient LLM communication through DSL commands ║
133
+ ║ 70-88% token reduction | Modular architecture ║
134
+ ╚══════════════════════════════════════════════════════════════╝
135
+
136
+ Type .help for available commands | .exit to quit
137
+ """
138
+ print(banner)
139
+
140
+ def print_help(self):
141
+ """Print help message."""
142
+ help_text = """
143
+ CompText REPL Commands:
144
+ .help Show this help message
145
+ .commands List all available CompText commands
146
+ .context Show current execution context
147
+ .clear Clear execution context
148
+ .history Show command history
149
+ .exit, .quit Exit the REPL
150
+
151
+ CompText Syntax Examples:
152
+ @A:compress The quick brown fox jumps over the lazy dog
153
+ @B:analyze[complexity, maintainability] def foo(): pass
154
+ @C:format[type=markdown] # My Title\\nContent here
155
+ @AUTOML[task=classification, metric=f1]
156
+ @EXTRACT[source=db] + @TRANSFORM[clean=true] + @LOAD[dest=warehouse]
157
+
158
+ Module Codes:
159
+ A - Core Commands │ H - Database
160
+ B - Analysis │ I - Security
161
+ C - Formatting │ J - DevOps
162
+ D - AI Control │ K - Frontend/UI
163
+ E - ML Pipelines │ L - ETL
164
+ F - Documentation │ M - MCP Integration
165
+ G - Testing │
166
+
167
+ Tips:
168
+ - Press Tab for autocomplete
169
+ - Use arrow keys to navigate history
170
+ - Chain commands with + operator
171
+ - Multi-line input: end line with \\ to continue
172
+ """
173
+ print(help_text)
174
+
175
+ def print_commands(self):
176
+ """Print all available commands."""
177
+ print("\n═══ Available Commands ═══\n")
178
+
179
+ for module in sorted(self.commands_by_module.keys()):
180
+ commands = self.commands_by_module[module]
181
+ if commands:
182
+ print(f"Module {module}:")
183
+ for cmd in sorted(commands):
184
+ print(f" @{module}:{cmd}")
185
+ print()
186
+
187
+ def print_context(self):
188
+ """Print current execution context."""
189
+ if not self.context:
190
+ print("Context is empty")
191
+ return
192
+
193
+ print("\n═══ Execution Context ═══\n")
194
+ for key, value in self.context.items():
195
+ # Truncate long values
196
+ value_str = str(value)
197
+ if len(value_str) > 100:
198
+ value_str = value_str[:97] + "..."
199
+ print(f"{key}: {value_str}")
200
+ print()
201
+
202
+ def print_history(self):
203
+ """Print command history."""
204
+ if not self.history:
205
+ print("No command history")
206
+ return
207
+
208
+ print("\n═══ Command History ═══\n")
209
+ for i, cmd in enumerate(self.history, 1):
210
+ print(f"{i:3d}: {cmd}")
211
+ print()
212
+
213
+ def format_result(self, result: ExecutionResult, command: CompTextCommand) -> str:
214
+ """Format execution result for display."""
215
+ if not result.success:
216
+ return f"❌ Error: {result.error}"
217
+
218
+ # Check if simulated
219
+ if result.metadata.get('simulated'):
220
+ return f"🔸 [Simulated] {result.result.get('message', 'Command executed')}"
221
+
222
+ # Format result based on type
223
+ result_data = result.result
224
+
225
+ if isinstance(result_data, dict):
226
+ # Pretty print dict results
227
+ lines = []
228
+ for key, value in result_data.items():
229
+ value_str = str(value)
230
+ if len(value_str) > 200:
231
+ value_str = value_str[:197] + "..."
232
+ lines.append(f" {key}: {value_str}")
233
+ return "✓ Result:\n" + "\n".join(lines)
234
+
235
+ elif isinstance(result_data, (list, tuple)):
236
+ if len(result_data) <= 5:
237
+ return f"✓ Result: {result_data}"
238
+ else:
239
+ preview = result_data[:5]
240
+ return f"✓ Result: {preview}... ({len(result_data)} items)"
241
+
242
+ else:
243
+ result_str = str(result_data)
244
+ if len(result_str) > 500:
245
+ result_str = result_str[:497] + "..."
246
+ return f"✓ Result: {result_str}"
247
+
248
+ def execute_repl_command(self, command: str) -> bool:
249
+ """Execute REPL meta-command.
250
+
251
+ Returns:
252
+ True to continue REPL, False to exit
253
+ """
254
+ command = command.strip()
255
+
256
+ if command in ['.exit', '.quit']:
257
+ return False
258
+
259
+ elif command == '.help':
260
+ self.print_help()
261
+
262
+ elif command == '.commands':
263
+ self.print_commands()
264
+
265
+ elif command == '.context':
266
+ self.print_context()
267
+
268
+ elif command == '.clear':
269
+ self.context.clear()
270
+ print("✓ Context cleared")
271
+
272
+ elif command == '.history':
273
+ self.print_history()
274
+
275
+ else:
276
+ print(f"Unknown command: {command}")
277
+ print("Type .help for available commands")
278
+
279
+ return True
280
+
281
+ def execute_comptext_command(self, command: str):
282
+ """Execute CompText DSL command."""
283
+ try:
284
+ # Parse and execute
285
+ commands = self.parser.parse(command)
286
+
287
+ if not commands:
288
+ print("❌ Failed to parse command")
289
+ return
290
+
291
+ # Execute all commands
292
+ results = self.executor.execute(command, context=self.context)
293
+
294
+ # Display results
295
+ for cmd, result in zip(commands, results):
296
+ print(self.format_result(result, cmd))
297
+
298
+ # Update context with successful results
299
+ if result.success and result.result:
300
+ self.context['_last_result'] = result.result
301
+
302
+ except Exception as e:
303
+ print(f"❌ Error: {e}")
304
+
305
+ def read_input(self) -> Optional[str]:
306
+ """Read input from user, supporting multi-line input.
307
+
308
+ Returns:
309
+ Complete command string, or None if EOF
310
+ """
311
+ try:
312
+ # Primary prompt
313
+ prompt = "comptext> " if not self.multiline_buffer else " ... "
314
+
315
+ line = input(prompt)
316
+
317
+ # Check for multi-line continuation
318
+ if line.endswith('\\'):
319
+ self.multiline_buffer.append(line[:-1]) # Remove trailing \
320
+ return self.read_input() # Recursive read
321
+
322
+ # Complete multi-line input
323
+ if self.multiline_buffer:
324
+ self.multiline_buffer.append(line)
325
+ complete = ' '.join(self.multiline_buffer)
326
+ self.multiline_buffer.clear()
327
+ return complete
328
+
329
+ return line
330
+
331
+ except EOFError:
332
+ return None
333
+ except KeyboardInterrupt:
334
+ # Clear multi-line buffer on Ctrl+C
335
+ if self.multiline_buffer:
336
+ self.multiline_buffer.clear()
337
+ print("\n(Multi-line input cancelled)")
338
+ return ""
339
+ print() # New line
340
+ return None
341
+
342
+ def run(self):
343
+ """Run the REPL main loop."""
344
+ self.print_banner()
345
+
346
+ while True:
347
+ try:
348
+ # Read command
349
+ command = self.read_input()
350
+
351
+ # Handle EOF (Ctrl+D) or exit
352
+ if command is None:
353
+ print("\nGoodbye!")
354
+ break
355
+
356
+ # Skip empty lines
357
+ command = command.strip()
358
+ if not command:
359
+ continue
360
+
361
+ # Add to history
362
+ self.history.append(command)
363
+
364
+ # Execute REPL command
365
+ if command.startswith('.'):
366
+ should_continue = self.execute_repl_command(command)
367
+ if not should_continue:
368
+ print("\nGoodbye!")
369
+ break
370
+
371
+ # Execute CompText command
372
+ elif command.startswith('@'):
373
+ self.execute_comptext_command(command)
374
+
375
+ else:
376
+ print("Commands must start with @ or .")
377
+ print("Type .help for usage information")
378
+
379
+ except KeyboardInterrupt:
380
+ print("\n(Use .exit or Ctrl+D to quit)")
381
+ continue
382
+
383
+ except Exception as e:
384
+ print(f"❌ Unexpected error: {e}")
385
+ import traceback
386
+ traceback.print_exc()
387
+
388
+
389
+ def main():
390
+ """Entry point for REPL."""
391
+ import argparse
392
+
393
+ parser = argparse.ArgumentParser(
394
+ description="CompText Codex Interactive REPL"
395
+ )
396
+ parser.add_argument(
397
+ '--codex-dir',
398
+ default='codex',
399
+ help='Path to codex directory (default: ./codex)'
400
+ )
401
+
402
+ args = parser.parse_args()
403
+
404
+ # Create and run REPL
405
+ repl = CompTextREPL(codex_dir=args.codex_dir)
406
+ repl.run()
407
+
408
+
409
+ if __name__ == '__main__':
410
+ main()