ali-tool 0.1.0__tar.gz

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,144 @@
1
+ Metadata-Version: 2.3
2
+ Name: ali-tool
3
+ Version: 0.1.0
4
+ Summary: Action Language Interpreter - One command for every tool
5
+ Author: Fredrik Angelsen
6
+ Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
7
+ Requires-Dist: pyyaml>=6.0.2
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # ALI - Action Language Interpreter
12
+
13
+ [![PyPI version](https://badge.fury.io/py/ali-tool.svg)](https://pypi.org/project/ali-tool/)
14
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+
17
+ Compose complex tmux commands from regular phrases. Pure command aggregator - outputs commands, never executes them.
18
+
19
+ ## Quickstart
20
+
21
+ ```bash
22
+ # Install
23
+ uv tool install ali-tool # Recommended
24
+ # OR
25
+ pip install ali-tool
26
+
27
+ # Initialize tmux integration
28
+ ali --init tmux
29
+ # Then reload tmux: tmux source ~/.tmux.conf
30
+
31
+ # Use
32
+ ali GO .2 # → tmux select-pane -t .2
33
+ ali SPLIT pop # → tmux display-popup -w 80% -h 80% -d "$PWD" -E 'bash'
34
+ ali EDIT file.txt pop # → tmux display-popup ... 'micro file.txt'
35
+ ali WIDTH 012 # → Distribute panes evenly
36
+ ali ECHO ed? # → Popup editor, pipe output to send-keys
37
+ ```
38
+
39
+ Press `C-b a` in tmux to open ALI prompt after initialization.
40
+
41
+ ## Architecture
42
+
43
+ ALI is a pure command aggregator - it only outputs commands, never executes them.
44
+
45
+ ```
46
+ Input → Parser → Router → Resolver → Output
47
+ ↓ ↓ ↓
48
+ Grammar Commands Templates
49
+ ```
50
+
51
+ ### Core Components
52
+
53
+ - **Parser** - Tokenizes input using plugin grammars
54
+ - **Router** - Matches patterns to find commands
55
+ - **Resolver** - Expands templates with conditionals and services
56
+ - **Registry** - Manages plugins and their services
57
+
58
+ ### Template Engine
59
+
60
+ ```yaml
61
+ # Conditionals
62
+ exec: "{?target:tmux select-pane -t {target} && }command"
63
+
64
+ # Array lookups
65
+ exec: "{direction[left:-h -b,right:-h,up:-v -b,down:-v,pop:display-popup]}"
66
+
67
+ # Service composition
68
+ exec: "{split} 'micro {file}'" # Uses split service from tmux
69
+ ```
70
+
71
+ ## Plugin Development
72
+
73
+ Plugins are YAML-only data files that define grammar, commands, and services.
74
+
75
+ ```yaml
76
+ # plugin.yaml
77
+ name: example
78
+ version: 1.0
79
+ description: Example plugin
80
+ # Grammar
81
+ grammar:
82
+ item: {type: string}
83
+ direction: {values: [left, right, up, down, pop]}
84
+ # Commands
85
+ commands:
86
+ - match: {verb: ACTION, item: present}
87
+ exec: "{split} '{item}'"
88
+ # Services
89
+ services:
90
+ process: "tool --process"
91
+ # Selectors
92
+ selectors:
93
+ item?:
94
+ type: stream
95
+ exec: "selector"
96
+ ```
97
+
98
+ See the [plugin documentation](https://github.com/angelsen/ali/tree/master/src/ali/plugins) for more patterns.
99
+
100
+ ## Examples
101
+
102
+ ### Navigation
103
+ ```bash
104
+ ali GO .2 # Go to pane 2
105
+ ali GO :3 # Go to window 3
106
+ ali GO ? # Visual pane selector
107
+ ```
108
+
109
+ ### Splits & Layout
110
+ ```bash
111
+ ali SPLIT # Split right (default)
112
+ ali SPLIT left # Split left
113
+ ali SPLIT pop # Open popup shell
114
+ ali WIDTH 012 # Distribute panes evenly
115
+ ```
116
+
117
+ ### Editing
118
+ ```bash
119
+ ali EDIT file.txt # Edit in right split
120
+ ali EDIT file.txt pop # Edit in popup
121
+ ali VIEW file.txt # Read-only view
122
+ ```
123
+
124
+ ### Stream Operations
125
+ ```bash
126
+ ali ECHO ed? # Edit in popup, pipe to send-keys
127
+ ali COPY br? # Browse in popup, copy to clipboard
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ # Clone and install dev version
134
+ git clone https://github.com/angelsen/ali.git
135
+ cd ali
136
+ uv tool install -e .
137
+
138
+ # Test a command
139
+ ali GO .2
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT - see [LICENSE](LICENSE)
@@ -0,0 +1,134 @@
1
+ # ALI - Action Language Interpreter
2
+
3
+ [![PyPI version](https://badge.fury.io/py/ali-tool.svg)](https://pypi.org/project/ali-tool/)
4
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Compose complex tmux commands from regular phrases. Pure command aggregator - outputs commands, never executes them.
8
+
9
+ ## Quickstart
10
+
11
+ ```bash
12
+ # Install
13
+ uv tool install ali-tool # Recommended
14
+ # OR
15
+ pip install ali-tool
16
+
17
+ # Initialize tmux integration
18
+ ali --init tmux
19
+ # Then reload tmux: tmux source ~/.tmux.conf
20
+
21
+ # Use
22
+ ali GO .2 # → tmux select-pane -t .2
23
+ ali SPLIT pop # → tmux display-popup -w 80% -h 80% -d "$PWD" -E 'bash'
24
+ ali EDIT file.txt pop # → tmux display-popup ... 'micro file.txt'
25
+ ali WIDTH 012 # → Distribute panes evenly
26
+ ali ECHO ed? # → Popup editor, pipe output to send-keys
27
+ ```
28
+
29
+ Press `C-b a` in tmux to open ALI prompt after initialization.
30
+
31
+ ## Architecture
32
+
33
+ ALI is a pure command aggregator - it only outputs commands, never executes them.
34
+
35
+ ```
36
+ Input → Parser → Router → Resolver → Output
37
+ ↓ ↓ ↓
38
+ Grammar Commands Templates
39
+ ```
40
+
41
+ ### Core Components
42
+
43
+ - **Parser** - Tokenizes input using plugin grammars
44
+ - **Router** - Matches patterns to find commands
45
+ - **Resolver** - Expands templates with conditionals and services
46
+ - **Registry** - Manages plugins and their services
47
+
48
+ ### Template Engine
49
+
50
+ ```yaml
51
+ # Conditionals
52
+ exec: "{?target:tmux select-pane -t {target} && }command"
53
+
54
+ # Array lookups
55
+ exec: "{direction[left:-h -b,right:-h,up:-v -b,down:-v,pop:display-popup]}"
56
+
57
+ # Service composition
58
+ exec: "{split} 'micro {file}'" # Uses split service from tmux
59
+ ```
60
+
61
+ ## Plugin Development
62
+
63
+ Plugins are YAML-only data files that define grammar, commands, and services.
64
+
65
+ ```yaml
66
+ # plugin.yaml
67
+ name: example
68
+ version: 1.0
69
+ description: Example plugin
70
+ # Grammar
71
+ grammar:
72
+ item: {type: string}
73
+ direction: {values: [left, right, up, down, pop]}
74
+ # Commands
75
+ commands:
76
+ - match: {verb: ACTION, item: present}
77
+ exec: "{split} '{item}'"
78
+ # Services
79
+ services:
80
+ process: "tool --process"
81
+ # Selectors
82
+ selectors:
83
+ item?:
84
+ type: stream
85
+ exec: "selector"
86
+ ```
87
+
88
+ See the [plugin documentation](https://github.com/angelsen/ali/tree/master/src/ali/plugins) for more patterns.
89
+
90
+ ## Examples
91
+
92
+ ### Navigation
93
+ ```bash
94
+ ali GO .2 # Go to pane 2
95
+ ali GO :3 # Go to window 3
96
+ ali GO ? # Visual pane selector
97
+ ```
98
+
99
+ ### Splits & Layout
100
+ ```bash
101
+ ali SPLIT # Split right (default)
102
+ ali SPLIT left # Split left
103
+ ali SPLIT pop # Open popup shell
104
+ ali WIDTH 012 # Distribute panes evenly
105
+ ```
106
+
107
+ ### Editing
108
+ ```bash
109
+ ali EDIT file.txt # Edit in right split
110
+ ali EDIT file.txt pop # Edit in popup
111
+ ali VIEW file.txt # Read-only view
112
+ ```
113
+
114
+ ### Stream Operations
115
+ ```bash
116
+ ali ECHO ed? # Edit in popup, pipe to send-keys
117
+ ali COPY br? # Browse in popup, copy to clipboard
118
+ ```
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ # Clone and install dev version
124
+ git clone https://github.com/angelsen/ali.git
125
+ cd ali
126
+ uv tool install -e .
127
+
128
+ # Test a command
129
+ ali GO .2
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT - see [LICENSE](LICENSE)
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "ali-tool"
3
+ version = "0.1.0"
4
+ description = "Action Language Interpreter - One command for every tool"
5
+ readme = "README.md"
6
+ authors = [{ name = "Fredrik Angelsen", email = "fredrikangelsen@gmail.com" }]
7
+ requires-python = ">=3.12"
8
+ dependencies = ["pyyaml>=6.0.2"]
9
+
10
+ [project.scripts]
11
+ ali = "ali_tool.cli:main"
12
+
13
+ [build-system]
14
+ requires = ["uv_build>=0.8.8,<0.9.0"]
15
+ build-backend = "uv_build"
16
+
17
+ [tool.basedpyright]
18
+ typeCheckingMode = "standard"
@@ -0,0 +1,3 @@
1
+ """ALI - Action Language Interpreter."""
2
+
3
+ __version__ = "0.1.0"
@@ -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)