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.
- ali_tool-0.1.0/PKG-INFO +144 -0
- ali_tool-0.1.0/README.md +134 -0
- ali_tool-0.1.0/pyproject.toml +18 -0
- ali_tool-0.1.0/src/ali_tool/__init__.py +3 -0
- ali_tool-0.1.0/src/ali_tool/cli.py +219 -0
- ali_tool-0.1.0/src/ali_tool/core/__init__.py +13 -0
- ali_tool-0.1.0/src/ali_tool/core/logging.py +130 -0
- ali_tool-0.1.0/src/ali_tool/core/plugin.py +101 -0
- ali_tool-0.1.0/src/ali_tool/core/registry.py +62 -0
- ali_tool-0.1.0/src/ali_tool/core/resolver.py +347 -0
- ali_tool-0.1.0/src/ali_tool/core/router.py +286 -0
- ali_tool-0.1.0/src/ali_tool/plugins/broot/ali.hjson +36 -0
- ali_tool-0.1.0/src/ali_tool/plugins/broot/plugin.yaml +61 -0
- ali_tool-0.1.0/src/ali_tool/plugins/llms.txt +156 -0
- ali_tool-0.1.0/src/ali_tool/plugins/micro/plugin.yaml +50 -0
- ali_tool-0.1.0/src/ali_tool/plugins/tmux/plugin.yaml +156 -0
- ali_tool-0.1.0/src/ali_tool/plugins/tmux/scripts/distribute.py +295 -0
- ali_tool-0.1.0/src/ali_tool/plugins/tmux/tmux.conf.ali +14 -0
- ali_tool-0.1.0/src/ali_tool/scripts.py +90 -0
ali_tool-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/ali-tool/)
|
|
14
|
+
[](https://www.python.org/downloads/)
|
|
15
|
+
[](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)
|
ali_tool-0.1.0/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ALI - Action Language Interpreter
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ali-tool/)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](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,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)
|