ali-tool 0.1.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.
ali_tool/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ALI - Action Language Interpreter."""
2
+
3
+ __version__ = "0.1.0"
ali_tool/cli.py ADDED
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env python3
2
+ """ALI CLI - Main entry point."""
3
+
4
+ import sys
5
+ import os
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from .core import ServiceRegistry, Router
10
+ from .core.logging import ALILogger
11
+ from .scripts import execute_script
12
+
13
+ EXIT_SUCCESS = 0
14
+ EXIT_ERROR = 1
15
+ EXIT_COMMAND_NOT_FOUND = 127
16
+
17
+ ALI_VERSION = "2.0.0"
18
+
19
+
20
+ def find_plugins_dir() -> Path:
21
+ """Find plugins directory in package or user config."""
22
+ package_dir = Path(__file__).parent / "plugins"
23
+ if package_dir.exists():
24
+ return package_dir
25
+
26
+ user_plugins = Path.home() / ".config" / "ali" / "plugins"
27
+ if user_plugins.exists():
28
+ return user_plugins
29
+
30
+ return package_dir
31
+
32
+
33
+ def main():
34
+ """Main CLI entry point."""
35
+ if "--plugin-script" in sys.argv:
36
+ exit_code = execute_script(sys.argv)
37
+ sys.exit(exit_code)
38
+
39
+ parser = argparse.ArgumentParser(
40
+ description="ALI - Action Language Interpreter",
41
+ epilog="Examples:\n"
42
+ " ali GO .2 # Go to pane 2\n"
43
+ " ali SPLIT pop # Open popup shell\n"
44
+ " ali EDIT file.txt pop # Edit in popup\n"
45
+ " ali WIDTH 012 # Distribute panes evenly\n"
46
+ " ali ECHO ed? # Edit and pipe output\n",
47
+ formatter_class=argparse.RawDescriptionHelpFormatter,
48
+ )
49
+
50
+ parser.add_argument(
51
+ "--version",
52
+ action="version",
53
+ version=f"ALI {ALI_VERSION}",
54
+ help="Show version and exit",
55
+ )
56
+
57
+ parser.add_argument(
58
+ "--list-verbs",
59
+ action="store_true",
60
+ help="List all available verbs",
61
+ )
62
+
63
+ parser.add_argument(
64
+ "--list-services",
65
+ action="store_true",
66
+ help="List all available services",
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--list-grammar",
71
+ action="store_true",
72
+ help="List grammar definitions for all plugins",
73
+ )
74
+
75
+ parser.add_argument(
76
+ "--verbose",
77
+ "-v",
78
+ action="store_true",
79
+ help="Enable verbose logging to stderr",
80
+ )
81
+
82
+ parser.add_argument(
83
+ "--quiet",
84
+ "-q",
85
+ action="store_true",
86
+ help="Disable all logging",
87
+ )
88
+
89
+ parser.add_argument(
90
+ "--plugins-dir",
91
+ type=Path,
92
+ help="Directory containing plugin definitions",
93
+ )
94
+
95
+ parser.add_argument(
96
+ "--init",
97
+ metavar="PLUGIN",
98
+ help="Initialize plugin integration (e.g., --init tmux)",
99
+ )
100
+
101
+ parser.add_argument(
102
+ "command",
103
+ nargs="*",
104
+ help="ALI command to execute",
105
+ )
106
+
107
+ args = parser.parse_args()
108
+
109
+ log_level = os.environ.get("ALI_LOG_LEVEL", "")
110
+ if args.quiet:
111
+ logger = None
112
+ elif args.verbose or log_level.upper() in ["DEBUG", "VERBOSE"]:
113
+ logger = ALILogger(verbose=True)
114
+ else:
115
+ logger = ALILogger(verbose=False)
116
+
117
+ registry = ServiceRegistry(logger=logger)
118
+ plugins_dir = args.plugins_dir or find_plugins_dir()
119
+
120
+ if not plugins_dir.exists():
121
+ print(f"Error: Plugins directory not found: {plugins_dir}", file=sys.stderr)
122
+ sys.exit(EXIT_ERROR)
123
+
124
+ registry.load_plugins(plugins_dir)
125
+
126
+ if logger:
127
+ logger.registry = registry
128
+
129
+ if args.init:
130
+ found = False
131
+ for plugin in registry.plugins:
132
+ if plugin.name == args.init:
133
+ found = True
134
+ exit_code = plugin.init()
135
+ sys.exit(exit_code if exit_code == 0 else EXIT_ERROR)
136
+ if not found:
137
+ print(f"Error: Plugin '{args.init}' not found", file=sys.stderr)
138
+ print(
139
+ f"Available plugins: {', '.join(p.name for p in registry.plugins)}",
140
+ file=sys.stderr,
141
+ )
142
+ sys.exit(EXIT_ERROR)
143
+
144
+ if args.list_services:
145
+ print("Available services:", file=sys.stderr)
146
+ for service, providers in sorted(registry.providers.items()):
147
+ provider_names = [p.name for p in providers]
148
+ print(
149
+ f" {service:20} provided by: {', '.join(provider_names)}",
150
+ file=sys.stderr,
151
+ )
152
+ sys.exit(EXIT_SUCCESS)
153
+
154
+ if args.list_grammar:
155
+ print("Grammar definitions by plugin:", file=sys.stderr)
156
+ for plugin in registry.plugins:
157
+ if plugin.grammar:
158
+ print(f"\n{plugin.name}:", file=sys.stderr)
159
+ for field, grammar in plugin.grammar.items():
160
+ if "pattern" in grammar:
161
+ pattern = grammar["pattern"]
162
+ if len(pattern) > 40:
163
+ pattern = pattern[:40] + "..."
164
+ print(f" {field:15} pattern: {pattern}", file=sys.stderr)
165
+ elif "values" in grammar:
166
+ values_str = ", ".join(grammar["values"][:3])
167
+ if len(grammar["values"]) > 3:
168
+ values_str += "..."
169
+ print(f" {field:15} values: [{values_str}]", file=sys.stderr)
170
+ elif "type" in grammar:
171
+ print(f" {field:15} type: {grammar['type']}", file=sys.stderr)
172
+ sys.exit(EXIT_SUCCESS)
173
+
174
+ if args.list_verbs:
175
+ if registry.verb_index:
176
+ print("Available verbs:", file=sys.stderr)
177
+ for verb, plugin_list in sorted(registry.verb_index.items()):
178
+ if plugin_list:
179
+ plugin_names = ", ".join(p.name for p in plugin_list)
180
+ print(f" {verb:12} ({plugin_names})", file=sys.stderr)
181
+ else:
182
+ print("No verbs available (no plugins loaded)", file=sys.stderr)
183
+ sys.exit(EXIT_SUCCESS)
184
+
185
+ if not args.command:
186
+ parser.print_help()
187
+ sys.exit(EXIT_ERROR)
188
+
189
+ command_str = " ".join(args.command)
190
+
191
+ router = Router(registry)
192
+ result = router.execute(command_str)
193
+
194
+ if logger:
195
+ tokens = router.last_tokens if hasattr(router, "last_tokens") else None
196
+ state = router.last_state if hasattr(router, "last_state") else None
197
+ success = not (result.startswith("Error:") or result.startswith("Unknown"))
198
+ logger.log_command(
199
+ command_str,
200
+ tokens=tokens,
201
+ state=state,
202
+ result=result if success else None,
203
+ error=result if not success else None,
204
+ success=success,
205
+ )
206
+
207
+ if result.startswith("Error:") or result.startswith("Unknown"):
208
+ print(result, file=sys.stderr)
209
+ exit_code = (
210
+ EXIT_COMMAND_NOT_FOUND if result.startswith("Unknown") else EXIT_ERROR
211
+ )
212
+ sys.exit(exit_code)
213
+
214
+ print(result)
215
+ sys.exit(EXIT_SUCCESS)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()
@@ -0,0 +1,13 @@
1
+ """ALI Core - Service-oriented plugin architecture."""
2
+
3
+ from .plugin import Plugin
4
+ from .registry import ServiceRegistry
5
+ from .resolver import resolve_command
6
+ from .router import Router
7
+
8
+ __all__ = [
9
+ "Plugin",
10
+ "ServiceRegistry",
11
+ "resolve_command",
12
+ "Router",
13
+ ]
@@ -0,0 +1,130 @@
1
+ """Centralized logging for ALI - plugin-driven environment capture."""
2
+
3
+ import logging
4
+ import json
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ import os
8
+ import uuid
9
+
10
+
11
+ class ALILogger:
12
+ """Structured logging for pattern analysis."""
13
+
14
+ def __init__(self, verbose: bool = False, registry=None):
15
+ self.session_id = self._generate_session_id()
16
+ self.log_dir = Path.home() / ".local/share/ali/logs"
17
+ self.log_dir.mkdir(parents=True, exist_ok=True)
18
+ self.registry = registry
19
+
20
+ self._setup_python_logging(verbose)
21
+
22
+ def _generate_session_id(self) -> str:
23
+ """Generate unique session ID with timestamp and UUID."""
24
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
25
+ unique = str(uuid.uuid4())[:8]
26
+ return f"{timestamp}_{unique}"
27
+
28
+ def _setup_python_logging(self, verbose: bool):
29
+ """Configure Python logging handlers and formatters."""
30
+ self.logger = logging.getLogger("ali")
31
+ self.logger.setLevel(logging.DEBUG)
32
+
33
+ file_handler = logging.FileHandler(
34
+ self.log_dir / f"debug_{self.session_id}.log"
35
+ )
36
+ file_handler.setLevel(logging.DEBUG)
37
+ file_formatter = logging.Formatter(
38
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
39
+ )
40
+ file_handler.setFormatter(file_formatter)
41
+ self.logger.addHandler(file_handler)
42
+
43
+ if verbose:
44
+ import sys
45
+
46
+ console_handler = logging.StreamHandler(sys.stderr)
47
+ console_handler.setLevel(logging.DEBUG)
48
+ console_formatter = logging.Formatter("%(levelname)s: %(message)s")
49
+ console_handler.setFormatter(console_formatter)
50
+ self.logger.addHandler(console_handler)
51
+
52
+ def _collect_plugin_env(self) -> dict:
53
+ """Collect environment variables defined by plugins."""
54
+ env = {}
55
+
56
+ if not self.registry:
57
+ return env
58
+
59
+ for plugin in self.registry.plugins:
60
+ metadata = plugin.config.get("metadata", {})
61
+ env_config = metadata.get("environment", {})
62
+
63
+ for var_list in ["requires", "optional", "captures"]:
64
+ for var in env_config.get(var_list, []):
65
+ if var in os.environ:
66
+ env[var] = os.environ[var]
67
+
68
+ return env
69
+
70
+ def log_command(
71
+ self,
72
+ command_str: str,
73
+ tokens: list | None = None,
74
+ state: dict | None = None,
75
+ result: str | None = None,
76
+ error: str | None = None,
77
+ success: bool = True,
78
+ ):
79
+ """Log command execution with context and results.
80
+
81
+ Args:
82
+ command_str: Raw command string
83
+ tokens: Parsed command tokens
84
+ state: Command state after parsing
85
+ result: Command result if successful
86
+ error: Error message if failed
87
+ success: Whether command executed successfully
88
+ """
89
+ event = {
90
+ "timestamp": datetime.now().isoformat(),
91
+ "session_id": self.session_id,
92
+ "command_raw": command_str,
93
+ "tokens": tokens,
94
+ "state": state,
95
+ "result": result,
96
+ "error": error,
97
+ "success": success,
98
+ "env": self._collect_plugin_env(),
99
+ }
100
+
101
+ with open(self.log_dir / "commands.jsonl", "a") as f:
102
+ f.write(json.dumps(event) + "\n")
103
+
104
+ if success:
105
+ self.logger.debug(f"Command: {command_str} -> {result}")
106
+ else:
107
+ self.logger.error(f"Command failed: {command_str} -> {error}")
108
+
109
+ def log_plugin_load(
110
+ self, plugin_name: str, success: bool, error: str | None = None
111
+ ):
112
+ """Log plugin loading success or failure.
113
+
114
+ Args:
115
+ plugin_name: Name of the plugin
116
+ success: Whether plugin loaded successfully
117
+ error: Error message if loading failed
118
+ """
119
+ if success:
120
+ self.logger.debug(f"Loaded plugin: {plugin_name}")
121
+ else:
122
+ self.logger.warning(f"Failed to load plugin {plugin_name}: {error}")
123
+
124
+ def debug(self, message: str):
125
+ """Log debug message."""
126
+ self.logger.debug(message)
127
+
128
+ def error(self, message: str):
129
+ """Log error message."""
130
+ self.logger.error(message)
@@ -0,0 +1,101 @@
1
+ """Plugin - Loads and holds plugin YAML configuration."""
2
+
3
+ import sys
4
+ import shutil
5
+ import subprocess
6
+ import yaml
7
+ from pathlib import Path
8
+
9
+
10
+ class Plugin:
11
+ """A plugin loaded from YAML. Just data, no logic."""
12
+
13
+ def __init__(self, yaml_path: Path):
14
+ self.path = yaml_path
15
+ self.name = yaml_path.parent.name
16
+
17
+ with open(yaml_path) as f:
18
+ self.config = yaml.safe_load(f)
19
+
20
+ self.version = self.config.get("version", "1.0")
21
+ self.description = self.config.get("description", "")
22
+ self.provides = self.config.get("provides", {})
23
+ self.requires = self.config.get("requires", [])
24
+ self.grammar = self.config.get("grammar", {})
25
+
26
+ vocab = self.config.get("vocabulary", {})
27
+ self.verbs = set(vocab.get("verbs", []))
28
+
29
+ self.expectations = self.config.get("expectations", {})
30
+ self.inference = self.config.get("inference", [])
31
+ self.commands = self.config.get("commands", [])
32
+ self.selectors = self.config.get("selectors", {})
33
+ self.context = self.config.get("context", {})
34
+ self.integration = self.config.get("integration", {})
35
+
36
+ def is_active(self) -> bool:
37
+ """Check if plugin should be active based on context requirements."""
38
+ if not self.context:
39
+ return True
40
+
41
+ if "requires_env" in self.context:
42
+ import os
43
+
44
+ required = self.context["requires_env"]
45
+ if isinstance(required, str):
46
+ required = [required]
47
+ for env_var in required:
48
+ if env_var not in os.environ:
49
+ return False
50
+
51
+ return True
52
+
53
+ def init(self) -> int:
54
+ """Initialize plugin integration files and show instructions.
55
+
56
+ Returns:
57
+ Exit code: 0 for success, 1 for error
58
+ """
59
+ if not self.integration:
60
+ print(
61
+ f"Plugin '{self.name}' has no integration configuration",
62
+ file=sys.stderr,
63
+ )
64
+ return 1
65
+
66
+ print(f"Initializing {self.name} integration...", file=sys.stderr)
67
+
68
+ for file_info in self.integration.get("files", []):
69
+ source = self.path.parent / file_info["source"]
70
+ target = Path(file_info["target"]).expanduser()
71
+
72
+ if not source.exists():
73
+ print(f"✗ Source file not found: {source}", file=sys.stderr)
74
+ continue
75
+
76
+ target.parent.mkdir(parents=True, exist_ok=True)
77
+ try:
78
+ shutil.copy(source, target)
79
+ print(f"✓ Copied {file_info['source']} to {target}", file=sys.stderr)
80
+ except Exception as e:
81
+ print(f"✗ Failed to copy {file_info['source']}: {e}", file=sys.stderr)
82
+
83
+ check_cmd = self.integration.get("check_command")
84
+ if check_cmd:
85
+ result = subprocess.run(check_cmd, shell=True, capture_output=True)
86
+ if result.returncode == 0:
87
+ print("✓ Already integrated", file=sys.stderr)
88
+ else:
89
+ print("✗ Not yet integrated", file=sys.stderr)
90
+ print("\nTo complete setup:", file=sys.stderr)
91
+ print(self.integration.get("instructions", ""), file=sys.stderr)
92
+
93
+ usage = self.integration.get("usage")
94
+ if usage:
95
+ print(f"\nUsage: {usage}", file=sys.stderr)
96
+
97
+ return 0
98
+
99
+ def __repr__(self):
100
+ """Return string representation of plugin."""
101
+ return f"Plugin({self.name} v{self.version})"
@@ -0,0 +1,62 @@
1
+ """Service Registry - Tracks plugin services and dependencies."""
2
+
3
+ from typing import Dict, List
4
+ from pathlib import Path
5
+ from .plugin import Plugin
6
+
7
+
8
+ class ServiceRegistry:
9
+ """Tracks what services plugins provide and require."""
10
+
11
+ def __init__(self, logger=None):
12
+ self.plugins: List[Plugin] = []
13
+ self.providers: Dict[str, List[Plugin]] = {}
14
+ self.verb_index: Dict[str, List[Plugin]] = {}
15
+ self.logger = logger
16
+
17
+ def load_plugins(self, plugin_dir: Path) -> None:
18
+ """Load all plugins from directory and build indices."""
19
+ self.plugins = []
20
+ self.providers = {}
21
+ self.verb_index = {}
22
+
23
+ for yaml_path in plugin_dir.glob("*/plugin.yaml"):
24
+ try:
25
+ plugin = Plugin(yaml_path)
26
+
27
+ if plugin.is_active():
28
+ self.register(plugin)
29
+ else:
30
+ if self.logger:
31
+ self.logger.debug(f"Skipped (inactive): {plugin.name}")
32
+
33
+ except Exception as e:
34
+ if self.logger:
35
+ self.logger.log_plugin_load(yaml_path.parent.name, False, str(e))
36
+
37
+ def register(self, plugin: Plugin) -> None:
38
+ """Register plugin and build service and verb indices."""
39
+ self.plugins.append(plugin)
40
+
41
+ if isinstance(plugin.provides, dict):
42
+ for service_name in plugin.provides:
43
+ if service_name not in self.providers:
44
+ self.providers[service_name] = []
45
+ self.providers[service_name].append(plugin)
46
+ elif isinstance(plugin.provides, list):
47
+ for service_name in plugin.provides:
48
+ if service_name not in self.providers:
49
+ self.providers[service_name] = []
50
+ self.providers[service_name].append(plugin)
51
+
52
+ for verb in plugin.verbs:
53
+ if verb not in self.verb_index:
54
+ self.verb_index[verb] = []
55
+ self.verb_index[verb].append(plugin)
56
+
57
+ if self.logger:
58
+ self.logger.log_plugin_load(plugin.name, True)
59
+
60
+ def get_plugins_for_verb(self, verb: str) -> List[Plugin]:
61
+ """Get all plugins that handle the given verb."""
62
+ return self.verb_index.get(verb.upper(), [])