kapsel-cli 0.6.3__tar.gz → 0.7.2__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.
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/PKG-INFO +1 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/__init__.py +1 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/cli.py +12 -11
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/carapace_installer.py +9 -15
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/add.py +24 -5
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/upgrade.py +59 -4
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/dispatcher.py +11 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/runner.py +103 -8
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/executor.py +11 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/catalog.py +11 -8
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/update_check.py +3 -12
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/prompt.py +6 -16
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/PKG-INFO +1 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/SOURCES.txt +6 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/pyproject.toml +1 -1
- kapsel_cli-0.7.2/tests/test_ai_spec_sanitizer.py +144 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_carapace_installer.py +5 -5
- kapsel_cli-0.7.2/tests/test_ctrl_b_roaming.py +69 -0
- kapsel_cli-0.7.2/tests/test_kp_enhanced.py +111 -0
- kapsel_cli-0.7.2/tests/test_multiline_passthrough.py +83 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_ai.py +1 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_alias.py +94 -0
- kapsel_cli-0.7.2/tests/test_plugin_install_batch.py +95 -0
- kapsel_cli-0.7.2/tests/test_plugin_kssh.py +91 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_preview.py +1 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_upgrade.py +1 -1
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/README.md +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/__main__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/carapace_engine.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/completer.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/completion.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/config.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/datadir.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/help.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/language.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/restart.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/search.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/status.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/toggle.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/registry.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/spec_manager.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/model.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/registry.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/detector.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/engine.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/i18n.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/base.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/context.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/fetcher.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/hooks.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/manager.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/tools/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/tools/installer.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/tools/registry.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/i18n.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/en/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/en/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/kapsel.pot +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/help.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/tips.yaml +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/config.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/history.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/logger.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/migrate.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/__init__.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/banner.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/card.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/theme.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/tips.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/viewport.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/dependency_links.txt +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/entry_points.txt +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/requires.txt +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/top_level.txt +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/setup.cfg +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_add_concurrent.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_block_system.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_config.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_dir_nav.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_history.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_i18n.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_autopilot.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_backup.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_commit.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_fetcher.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_help.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_init.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_install.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_lang.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_mend.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_portal.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_queue.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_template.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_trans.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_restart_and_pypi.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_search_and_completions.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_spec_manager.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_toggle.py +0 -0
- {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_tools_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions.
|
|
5
5
|
Author-email: MrEiu <k648888@vip.qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -102,17 +102,18 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
102
102
|
if not stripped:
|
|
103
103
|
continue
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
if "\n" not in stripped:
|
|
106
|
+
normalized = " ".join(stripped.lower().split())
|
|
107
|
+
if normalized in ("exit", "quit", "bye"):
|
|
108
|
+
engine.history_mgr.record_command(command=user_input, mode="builtin")
|
|
109
|
+
os.environ.pop("KAPSEL_ACTIVE", None)
|
|
110
|
+
console.print("[dim]Exited Kapsel.[/]")
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
if normalized in ("restart", "kapsel restart", "reload"):
|
|
114
|
+
engine.history_mgr.record_command(command=user_input, mode="builtin")
|
|
115
|
+
from kapsel.completion.kps.builtins.restart import handle_restart
|
|
116
|
+
return handle_restart(console=console)
|
|
116
117
|
|
|
117
118
|
try:
|
|
118
119
|
result = engine.dispatch(user_input)
|
|
@@ -192,9 +192,10 @@ def install_carapace(
|
|
|
192
192
|
|
|
193
193
|
def ensure_carapace_installed(console: Optional[Console] = None) -> bool:
|
|
194
194
|
"""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
Checks if Carapace completion engine is available.
|
|
196
|
+
If not installed, prints a lightweight non-blocking notice and enters immediately.
|
|
197
|
+
Never blocks terminal startup with network downloads.
|
|
198
|
+
Users can install it anytime on demand using 'kapsel setup-completion'.
|
|
198
199
|
"""
|
|
199
200
|
from kapsel.completion.carapace_engine import get_carapace_engine
|
|
200
201
|
|
|
@@ -203,16 +204,9 @@ def ensure_carapace_installed(console: Optional[Console] = None) -> bool:
|
|
|
203
204
|
return True
|
|
204
205
|
|
|
205
206
|
con = console or Console(legacy_windows=False)
|
|
206
|
-
con.print(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
except Exception as e:
|
|
212
|
-
logger.warning(f"Failed to auto-bootstrap Carapace: {e}")
|
|
213
|
-
con.print(
|
|
214
|
-
f"[yellow]Notice: Could not automatically setup Carapace ({e}). "
|
|
215
|
-
f"Continuing in basic autocompletion mode.[/]\n"
|
|
216
|
-
)
|
|
217
|
-
return False
|
|
207
|
+
con.print(
|
|
208
|
+
"[dim]ℹ Notice: Carapace completion engine not detected. Running in standard autocompletion mode.[/]\n"
|
|
209
|
+
"[dim] (Optional: run '[bold cyan]kapsel setup-completion[/]' to enable 1,000+ tool completions)[/]\n"
|
|
210
|
+
)
|
|
211
|
+
return False
|
|
218
212
|
|
|
@@ -60,6 +60,26 @@ def _check_plugin_already_installed(
|
|
|
60
60
|
return False, None, None
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _get_installed_plugin_names(is_dev: bool = False) -> Set[str]:
|
|
64
|
+
"""Returns set of all installed and enabled plugin names."""
|
|
65
|
+
installed = set()
|
|
66
|
+
global_plugins_dir = get_kapsel_dir() / "plugins"
|
|
67
|
+
if global_plugins_dir.is_dir():
|
|
68
|
+
for p in global_plugins_dir.iterdir():
|
|
69
|
+
if p.is_dir() and ((p / "__init__.py").exists() or (p / "plugin.py").exists()):
|
|
70
|
+
installed.add(p.name.lower())
|
|
71
|
+
if is_dev:
|
|
72
|
+
dev_dir = Path.cwd() / "plugins"
|
|
73
|
+
if dev_dir.is_dir():
|
|
74
|
+
for p in dev_dir.iterdir():
|
|
75
|
+
if p.is_dir() and ((p / "__init__.py").exists() or (p / "plugin.py").exists()):
|
|
76
|
+
installed.add(p.name.lower())
|
|
77
|
+
cfg = load_config()
|
|
78
|
+
for ep in (cfg.enabled_plugins or []):
|
|
79
|
+
installed.add(ep.lower())
|
|
80
|
+
return installed
|
|
81
|
+
|
|
82
|
+
|
|
63
83
|
def _install_single_plugin(
|
|
64
84
|
target_raw: str,
|
|
65
85
|
con: Console,
|
|
@@ -212,14 +232,14 @@ def _handle_add_batch_concurrent(
|
|
|
212
232
|
already_enabled = set(p.lower() for p in (cfg.enabled_plugins or []))
|
|
213
233
|
|
|
214
234
|
# Partition targets into uninstalled vs already-installed
|
|
235
|
+
installed_names = _get_installed_plugin_names()
|
|
215
236
|
uninstalled: List[str] = []
|
|
216
237
|
already_done: List[str] = []
|
|
217
238
|
for t in targets:
|
|
218
239
|
t_clean = t.strip()
|
|
219
240
|
if not t_clean:
|
|
220
241
|
continue
|
|
221
|
-
|
|
222
|
-
if is_inst or t_clean.lower() in already_enabled:
|
|
242
|
+
if t_clean.lower() in installed_names:
|
|
223
243
|
already_done.append(t_clean)
|
|
224
244
|
else:
|
|
225
245
|
uninstalled.append(t_clean)
|
|
@@ -372,12 +392,11 @@ def _handle_add_all(con: Console, is_dev: bool = False, max_workers: int = 4) ->
|
|
|
372
392
|
cfg = load_config()
|
|
373
393
|
already_enabled = set(p.lower() for p in (cfg.enabled_plugins or []))
|
|
374
394
|
|
|
375
|
-
|
|
395
|
+
installed_names = _get_installed_plugin_names(is_dev=is_dev)
|
|
376
396
|
uninstalled: List[str] = []
|
|
377
397
|
already_installed: List[str] = []
|
|
378
398
|
for p in ordered_plugins:
|
|
379
|
-
|
|
380
|
-
if is_inst or p.lower() in already_enabled:
|
|
399
|
+
if p.lower() in installed_names:
|
|
381
400
|
already_installed.append(p)
|
|
382
401
|
else:
|
|
383
402
|
uninstalled.append(p)
|
|
@@ -394,17 +394,38 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
|
|
|
394
394
|
con.print(f"[dim]{_('Cancelled.')}[/]\n")
|
|
395
395
|
return 0
|
|
396
396
|
|
|
397
|
-
# Perform plugin update download
|
|
397
|
+
# Perform plugin update download (Stage 1: Plugin package itself)
|
|
398
398
|
dest_dir = info["dir"] or (get_kapsel_dir() / "plugins" / target_plugin)
|
|
399
399
|
con.print(f"[dim]{_('Downloading and updating plugin package...')}[/]")
|
|
400
400
|
if fetch_plugin_from_remote(target_plugin, dest_dir, con):
|
|
401
|
-
|
|
401
|
+
con.print(f"[bold #10b981]✔ {_('Plugin')} '[bold #00f0ff]{target_plugin}[/]' {_('successfully updated to')} v{info['latest']}![/]\n")
|
|
402
|
+
|
|
403
|
+
# Stage 2: Prompt for optional dependency installation
|
|
404
|
+
has_deps = (dest_dir / "tools.yaml").is_file() or (dest_dir / "install.py").is_file()
|
|
405
|
+
if has_deps:
|
|
406
|
+
should_install_deps = auto_yes
|
|
407
|
+
if not auto_yes:
|
|
408
|
+
ans = prompt_user_choice(
|
|
409
|
+
f"Install external dependencies for '{target_plugin}'? (External tool installation may fail) ([Enter] Yes / [s] Skip): ",
|
|
410
|
+
default="y",
|
|
411
|
+
)
|
|
412
|
+
should_install_deps = ans in ("y", "yes")
|
|
413
|
+
|
|
414
|
+
if should_install_deps:
|
|
415
|
+
con.print(f"[dim]Installing tool dependencies for '[bold #00f0ff]{target_plugin}[/]'...[/]")
|
|
416
|
+
ok = install_plugin_dependencies(dest_dir, con)
|
|
417
|
+
if ok:
|
|
418
|
+
con.print(f"[bold #10b981]✔ Dependencies for '{target_plugin}' installed successfully.[/]\n")
|
|
419
|
+
else:
|
|
420
|
+
con.print(f"[yellow]⚠ Warning: Tool dependency installation encountered errors. You can run 'kps install {target_plugin}' or 'kps mend' to check and repair.[/]\n")
|
|
421
|
+
else:
|
|
422
|
+
con.print(f"[dim]Skipped dependency installation. You can run 'kps install {target_plugin}' or 'kps mend' anytime.[/]\n")
|
|
423
|
+
|
|
402
424
|
try:
|
|
403
425
|
from kapsel.completion.spec_manager import CarapaceSpecManager
|
|
404
426
|
CarapaceSpecManager().sync_specs()
|
|
405
427
|
except Exception:
|
|
406
428
|
pass
|
|
407
|
-
con.print(f"[bold #10b981]✔ {_('Plugin')} '[bold #00f0ff]{target_plugin}[/]' {_('successfully updated to')} v{info['latest']}![/]\n")
|
|
408
429
|
return 0
|
|
409
430
|
else:
|
|
410
431
|
con.print(f"[bold #f43f5e]✘ {_('Failed to download update for')} '{target_plugin}'.[/]\n")
|
|
@@ -542,7 +563,6 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
|
|
|
542
563
|
|
|
543
564
|
try:
|
|
544
565
|
if fetch_plugin_from_remote(name, dest_dir, con):
|
|
545
|
-
install_plugin_dependencies(dest_dir, con)
|
|
546
566
|
with _CONSOLE_LOCK:
|
|
547
567
|
con.print(f" [bold #10b981]✔ {_('Updated')} {name} {_('to')} v{latest_ver}[/]")
|
|
548
568
|
return True, name, f"v{latest_ver}"
|
|
@@ -572,6 +592,41 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
|
|
|
572
592
|
else:
|
|
573
593
|
failed_plugins.append((name, detail))
|
|
574
594
|
|
|
595
|
+
# Stage 2: Prompt to install dependencies for updated plugins
|
|
596
|
+
plugins_with_deps: List[Tuple[str, Path]] = []
|
|
597
|
+
for name in succeeded_plugins:
|
|
598
|
+
p_dir = None
|
|
599
|
+
for p in upgradable_plugins:
|
|
600
|
+
if p["name"] == name:
|
|
601
|
+
p_dir = p.get("dir") or (get_kapsel_dir() / "plugins" / name)
|
|
602
|
+
break
|
|
603
|
+
if p_dir and ((p_dir / "tools.yaml").is_file() or (p_dir / "install.py").is_file()):
|
|
604
|
+
plugins_with_deps.append((name, p_dir))
|
|
605
|
+
|
|
606
|
+
if plugins_with_deps:
|
|
607
|
+
con.print(f"\n[bold #00f0ff]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]")
|
|
608
|
+
con.print(f"[bold #00f0ff]📦 Plugin Dependencies Setup[/]")
|
|
609
|
+
con.print(f"[dim]{len(plugins_with_deps)} updated plugin(s) contain external tool dependencies.[/]")
|
|
610
|
+
|
|
611
|
+
should_install_all_deps = auto_yes
|
|
612
|
+
if not auto_yes:
|
|
613
|
+
dep_ans = prompt_user_choice(
|
|
614
|
+
f"Install external dependencies for {len(plugins_with_deps)} plugin(s)? (External tool installation may fail) ([Enter] Yes / [s] Skip): ",
|
|
615
|
+
default="y",
|
|
616
|
+
)
|
|
617
|
+
should_install_all_deps = dep_ans in ("y", "yes")
|
|
618
|
+
|
|
619
|
+
if should_install_all_deps:
|
|
620
|
+
for p_name, p_dir in plugins_with_deps:
|
|
621
|
+
con.print(f"\n[dim]Installing tool dependencies for '[bold #00f0ff]{p_name}[/]'...[/]")
|
|
622
|
+
ok = install_plugin_dependencies(p_dir, con)
|
|
623
|
+
if ok:
|
|
624
|
+
con.print(f" [bold #10b981]✔ Dependencies for '{p_name}' installed successfully.[/]")
|
|
625
|
+
else:
|
|
626
|
+
con.print(f" [yellow]⚠ Warning: Dependency installation for '{p_name}' encountered errors. Run 'kps install {p_name}' or 'kps mend' to check and repair.[/]")
|
|
627
|
+
else:
|
|
628
|
+
con.print("[dim]Skipped dependency installation. You can run 'kps install <plugin>' or 'kps mend' anytime.[/]")
|
|
629
|
+
|
|
575
630
|
# Re-sync completion specifications
|
|
576
631
|
try:
|
|
577
632
|
from kapsel.completion.spec_manager import CarapaceSpecManager
|
|
@@ -133,6 +133,7 @@ def _handle_kp_command(
|
|
|
133
133
|
con.print("[dim]Executes multiple commands or pasted blocks asynchronously in the background.[/]\n")
|
|
134
134
|
con.print("[bold white]Usage:[/]")
|
|
135
135
|
con.print(" [bold #a855f7]kp <commands...>[/] Execute commands sequentially in background (atomic, non-blocking)")
|
|
136
|
+
con.print(" [bold #a855f7]kp -s <commands...>[/] Execute commands sequentially in background (explicit sequential)")
|
|
136
137
|
con.print(" [bold #a855f7]kp -c <commands...>[/] Execute commands concurrently in background parallel tracks")
|
|
137
138
|
con.print(" [bold #a855f7]kp attach [id][/] Attach to running block as interactive sub-terminal (二级终端)")
|
|
138
139
|
con.print(" [bold #a855f7]kp status[/] List session blocks, running status, PID, and exit codes")
|
|
@@ -140,6 +141,7 @@ def _handle_kp_command(
|
|
|
140
141
|
con.print(" [bold #a855f7]kp stop <id>[/] Terminate a running background block\n")
|
|
141
142
|
con.print("[bold white]Examples:[/]")
|
|
142
143
|
con.print(" kp npm run dev (Runs dev server in background, prompt stays clean)")
|
|
144
|
+
con.print(" kp -s git pull\\npnpm build (Runs tasks sequentially in background)")
|
|
143
145
|
con.print(" kp -c pnpm build:app\\npnpm build:docs (Runs build tasks concurrently in parallel)\n")
|
|
144
146
|
return 0
|
|
145
147
|
|
|
@@ -262,6 +264,15 @@ def _handle_kp_command(
|
|
|
262
264
|
elif sub.startswith("-c\n") or sub.startswith("--concurrent\n"):
|
|
263
265
|
is_concurrent = True
|
|
264
266
|
body = sub.split("\n", 1)[1]
|
|
267
|
+
elif sub.startswith("-s ") or sub.startswith("--sequential "):
|
|
268
|
+
is_concurrent = False
|
|
269
|
+
body = sub.split(" ", 1)[1]
|
|
270
|
+
elif sub == "-s" or sub == "--sequential":
|
|
271
|
+
con.print("[bold #f43f5e]Error:[/] Please provide commands to execute with 'kp -s'.\n")
|
|
272
|
+
return 1
|
|
273
|
+
elif sub.startswith("-s\n") or sub.startswith("--sequential\n"):
|
|
274
|
+
is_concurrent = False
|
|
275
|
+
body = sub.split("\n", 1)[1]
|
|
265
276
|
|
|
266
277
|
commands = split_commands(body)
|
|
267
278
|
if not commands:
|
|
@@ -20,19 +20,114 @@ from kapsel.core.block.model import BlockStatus, CommandBlock
|
|
|
20
20
|
from kapsel.core.block.registry import get_block_registry
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _normalize_continuation_spaces(text: str) -> str:
|
|
24
|
+
"""Replaces unquoted newlines (from continuation lines) with spaces and collapses indentation."""
|
|
25
|
+
if "\n" not in text:
|
|
26
|
+
return text
|
|
27
|
+
|
|
28
|
+
result = []
|
|
29
|
+
in_single = False
|
|
30
|
+
in_double = False
|
|
31
|
+
i = 0
|
|
32
|
+
while i < len(text):
|
|
33
|
+
ch = text[i]
|
|
34
|
+
if (ch == "\\" or ch == "`") and not in_single and i + 1 < len(text):
|
|
35
|
+
result.append(ch)
|
|
36
|
+
result.append(text[i + 1])
|
|
37
|
+
i += 2
|
|
38
|
+
continue
|
|
39
|
+
if ch == "'" and not in_double:
|
|
40
|
+
in_single = not in_single
|
|
41
|
+
result.append(ch)
|
|
42
|
+
elif ch == '"' and not in_single:
|
|
43
|
+
in_double = not in_double
|
|
44
|
+
result.append(ch)
|
|
45
|
+
elif ch == "\n" and not in_single and not in_double:
|
|
46
|
+
if result and result[-1] != " ":
|
|
47
|
+
result.append(" ")
|
|
48
|
+
while i + 1 < len(text) and text[i + 1] in (" ", "\t"):
|
|
49
|
+
i += 1
|
|
50
|
+
else:
|
|
51
|
+
result.append(ch)
|
|
52
|
+
i += 1
|
|
53
|
+
|
|
54
|
+
return "".join(result).strip()
|
|
55
|
+
|
|
56
|
+
|
|
23
57
|
def split_commands(raw_text: str) -> List[str]:
|
|
24
58
|
"""
|
|
25
|
-
Parses multi-line or delimited text into individual commands
|
|
26
|
-
|
|
59
|
+
Parses multi-line or delimited text into individual executable commands.
|
|
60
|
+
- Preserves newlines within unclosed quotes ('', "").
|
|
61
|
+
- Handles line continuation characters ('\\' in POSIX/Bash, '`' in PowerShell)
|
|
62
|
+
by joining them into a single command with continuation characters stripped.
|
|
63
|
+
- Handles trailing pipeline ('|') and logical ('&&', '||') continuations.
|
|
64
|
+
- Ignores empty whitespace lines.
|
|
65
|
+
- Does NOT filter '#' lines (leaves comments/commands intact for the shell).
|
|
27
66
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
if not raw_text or not raw_text.strip():
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
lines = raw_text.splitlines()
|
|
71
|
+
commands: List[str] = []
|
|
72
|
+
current_parts: List[str] = []
|
|
73
|
+
in_single_quote = False
|
|
74
|
+
in_double_quote = False
|
|
75
|
+
|
|
30
76
|
for line in lines:
|
|
31
|
-
|
|
32
|
-
if not
|
|
77
|
+
stripped = line.strip()
|
|
78
|
+
if not stripped and not current_parts and not in_single_quote and not in_double_quote:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Count quotes in this line, ignoring escaped quotes
|
|
82
|
+
i = 0
|
|
83
|
+
while i < len(line):
|
|
84
|
+
ch = line[i]
|
|
85
|
+
if (ch == "\\" or ch == "`") and not in_single_quote and i + 1 < len(line):
|
|
86
|
+
i += 2
|
|
87
|
+
continue
|
|
88
|
+
if ch == "'" and not in_double_quote:
|
|
89
|
+
in_single_quote = not in_single_quote
|
|
90
|
+
elif ch == '"' and not in_single_quote:
|
|
91
|
+
in_double_quote = not in_double_quote
|
|
92
|
+
i += 1
|
|
93
|
+
|
|
94
|
+
# If currently inside an open quote, line continues unconditionally
|
|
95
|
+
if in_single_quote or in_double_quote:
|
|
96
|
+
current_parts.append(line)
|
|
33
97
|
continue
|
|
34
|
-
|
|
35
|
-
|
|
98
|
+
|
|
99
|
+
# Outside quotes: check if line ends with a continuation token
|
|
100
|
+
r_stripped = line.rstrip()
|
|
101
|
+
is_continuation = False
|
|
102
|
+
continuation_clean = line
|
|
103
|
+
|
|
104
|
+
if r_stripped.endswith("\\"):
|
|
105
|
+
is_continuation = True
|
|
106
|
+
continuation_clean = r_stripped[:-1].rstrip()
|
|
107
|
+
elif r_stripped.endswith("`"):
|
|
108
|
+
is_continuation = True
|
|
109
|
+
continuation_clean = r_stripped[:-1].rstrip()
|
|
110
|
+
elif r_stripped.endswith("|") or r_stripped.endswith("&&") or r_stripped.endswith("||"):
|
|
111
|
+
is_continuation = True
|
|
112
|
+
continuation_clean = r_stripped
|
|
113
|
+
|
|
114
|
+
if is_continuation:
|
|
115
|
+
current_parts.append(continuation_clean)
|
|
116
|
+
else:
|
|
117
|
+
current_parts.append(line)
|
|
118
|
+
full_cmd = "\n".join(current_parts).strip()
|
|
119
|
+
full_cmd = _normalize_continuation_spaces(full_cmd)
|
|
120
|
+
if full_cmd:
|
|
121
|
+
commands.append(full_cmd)
|
|
122
|
+
current_parts = []
|
|
123
|
+
|
|
124
|
+
if current_parts:
|
|
125
|
+
leftover = "\n".join(current_parts).strip()
|
|
126
|
+
leftover = _normalize_continuation_spaces(leftover)
|
|
127
|
+
if leftover:
|
|
128
|
+
commands.append(leftover)
|
|
129
|
+
|
|
130
|
+
return commands
|
|
36
131
|
|
|
37
132
|
|
|
38
133
|
def _run_single_command_process(cmd: str, cwd: str) -> Tuple[int, str, int]:
|
|
@@ -81,6 +81,11 @@ class CommandExecutor:
|
|
|
81
81
|
|
|
82
82
|
def _handle_builtin(self, cmd: str) -> Optional[ExecutionSummary]:
|
|
83
83
|
"""Intercepts built-ins like cd, clear, cls."""
|
|
84
|
+
# Multi-line commands/scripts must bypass single-line built-in interception
|
|
85
|
+
# to prevent cmd.split() from collapsing lines into space-separated arguments.
|
|
86
|
+
if "\n" in cmd:
|
|
87
|
+
return None
|
|
88
|
+
|
|
84
89
|
parts = cmd.split()
|
|
85
90
|
if not parts:
|
|
86
91
|
return None
|
|
@@ -220,7 +225,12 @@ class CommandExecutor:
|
|
|
220
225
|
args = [exe, "-NoLogo", "-Command", cmd]
|
|
221
226
|
elif shell == "cmd":
|
|
222
227
|
exe = self.shell_path if self.shell_path and Path(self.shell_path).exists() else "cmd.exe"
|
|
223
|
-
|
|
228
|
+
effective_cmd = (
|
|
229
|
+
" & ".join(line.strip() for line in cmd.splitlines() if line.strip())
|
|
230
|
+
if "\n" in cmd
|
|
231
|
+
else cmd
|
|
232
|
+
)
|
|
233
|
+
args = [exe, "/c", effective_cmd]
|
|
224
234
|
else:
|
|
225
235
|
# Unix shells (bash, zsh, fish, sh)
|
|
226
236
|
exe = self.shell_path if self.shell_path and Path(self.shell_path).exists() else "/bin/sh"
|
|
@@ -53,12 +53,15 @@ def _resolve_catalog_paths():
|
|
|
53
53
|
if workspace_catalog not in paths:
|
|
54
54
|
paths.append(workspace_catalog)
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
global_catalog = get_kapsel_dir() / "plugins_catalog.json"
|
|
58
|
-
paths.append(global_catalog)
|
|
59
|
-
|
|
56
|
+
# 3. Global runtime plugins directory
|
|
60
57
|
global_in_plugins = get_kapsel_dir() / "plugins" / "catalog.json"
|
|
61
|
-
paths
|
|
58
|
+
if global_in_plugins not in paths:
|
|
59
|
+
paths.append(global_in_plugins)
|
|
60
|
+
|
|
61
|
+
# 4. Global root catalog fallback
|
|
62
|
+
global_root = get_kapsel_dir() / "catalog.json"
|
|
63
|
+
if global_root not in paths:
|
|
64
|
+
paths.append(global_root)
|
|
62
65
|
|
|
63
66
|
return paths
|
|
64
67
|
|
|
@@ -267,13 +270,13 @@ def update_plugin_catalog(console: Optional[Console] = None) -> Dict[str, str]:
|
|
|
267
270
|
except Exception as e:
|
|
268
271
|
con.print(f"[yellow]Warning: Failed to write {target_file}:[/] {e}")
|
|
269
272
|
|
|
270
|
-
# Also save to global data directory
|
|
271
|
-
global_cache = get_kapsel_dir() / "
|
|
273
|
+
# Also save to global data directory catalog.json
|
|
274
|
+
global_cache = get_kapsel_dir() / "plugins" / "catalog.json"
|
|
272
275
|
try:
|
|
273
276
|
global_cache.parent.mkdir(parents=True, exist_ok=True)
|
|
274
277
|
global_cache.write_text(json.dumps(rich_catalog, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
275
278
|
except Exception as e:
|
|
276
|
-
con.print(f"[yellow]Warning: Failed to write global
|
|
279
|
+
con.print(f"[yellow]Warning: Failed to write global catalog {global_cache}:[/] {e}")
|
|
277
280
|
|
|
278
281
|
flat_catalog = {k: v["description"] for k, v in rich_catalog.items()}
|
|
279
282
|
|
|
@@ -125,16 +125,7 @@ def get_pypi_upgrade_notice() -> Optional[str]:
|
|
|
125
125
|
return latest_ver
|
|
126
126
|
return None
|
|
127
127
|
|
|
128
|
-
# If no cache exists, launch
|
|
129
|
-
|
|
130
|
-
worker.start()
|
|
131
|
-
worker.join(timeout=0.6)
|
|
132
|
-
|
|
133
|
-
# Re-check cache after fast join
|
|
134
|
-
cached_after = _read_cached_info()
|
|
135
|
-
if cached_after is not None:
|
|
136
|
-
latest_ver = str(cached_after.get("latest_version", ""))
|
|
137
|
-
if parse_semver(latest_ver) > parse_semver(CURRENT_KAPSEL_VERSION):
|
|
138
|
-
return latest_ver
|
|
139
|
-
|
|
128
|
+
# If no cache exists, launch worker asynchronously in the background without blocking startup
|
|
129
|
+
threading.Thread(target=_background_fetch_worker, daemon=True).start()
|
|
140
130
|
return None
|
|
131
|
+
|
|
@@ -187,12 +187,13 @@ class BlockRoamingController:
|
|
|
187
187
|
|
|
188
188
|
def enter_roaming(self, buffer) -> None:
|
|
189
189
|
blocks = self.get_blocks()
|
|
190
|
+
self.is_active = True
|
|
191
|
+
self.saved_buffer_text = buffer.text
|
|
190
192
|
if not blocks:
|
|
193
|
+
self.current_index = -1
|
|
191
194
|
self.status_msg = "No command blocks in session history."
|
|
192
195
|
return
|
|
193
196
|
|
|
194
|
-
self.is_active = True
|
|
195
|
-
self.saved_buffer_text = buffer.text
|
|
196
197
|
self.current_index = len(blocks) - 1
|
|
197
198
|
self.status_msg = ""
|
|
198
199
|
self._sync_buffer_to_block(buffer)
|
|
@@ -467,21 +468,10 @@ def create_key_bindings(
|
|
|
467
468
|
@kb.add("c-b")
|
|
468
469
|
def _(event: KeyPressEvent) -> None:
|
|
469
470
|
"""
|
|
470
|
-
Ctrl+B:
|
|
471
|
-
|
|
472
|
-
Pressing Enter on a running block inside the navigator enters the live sub-terminal.
|
|
471
|
+
Ctrl+B: Toggles inline Block Roaming Mode directly in the prompt line,
|
|
472
|
+
without launching a disruptive full-screen viewport.
|
|
473
473
|
"""
|
|
474
|
-
|
|
475
|
-
from kapsel.ui.viewport import run_block_viewport_async
|
|
476
|
-
|
|
477
|
-
async def _launch() -> None:
|
|
478
|
-
async with in_terminal():
|
|
479
|
-
cmd = await run_block_viewport_async()
|
|
480
|
-
if cmd is not None:
|
|
481
|
-
event.current_buffer.text = cmd
|
|
482
|
-
event.current_buffer.cursor_position = len(cmd)
|
|
483
|
-
|
|
484
|
-
event.app.create_background_task(_launch())
|
|
474
|
+
roam_ctrl.toggle(event.current_buffer)
|
|
485
475
|
|
|
486
476
|
@kb.add("up", filter=is_roaming)
|
|
487
477
|
@kb.add("k", filter=is_roaming)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kapsel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions.
|
|
5
5
|
Author-email: MrEiu <k648888@vip.qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -92,12 +92,16 @@ kapsel_cli.egg-info/entry_points.txt
|
|
|
92
92
|
kapsel_cli.egg-info/requires.txt
|
|
93
93
|
kapsel_cli.egg-info/top_level.txt
|
|
94
94
|
tests/test_add_concurrent.py
|
|
95
|
+
tests/test_ai_spec_sanitizer.py
|
|
95
96
|
tests/test_block_system.py
|
|
96
97
|
tests/test_carapace_installer.py
|
|
97
98
|
tests/test_config.py
|
|
99
|
+
tests/test_ctrl_b_roaming.py
|
|
98
100
|
tests/test_dir_nav.py
|
|
99
101
|
tests/test_history.py
|
|
100
102
|
tests/test_i18n.py
|
|
103
|
+
tests/test_kp_enhanced.py
|
|
104
|
+
tests/test_multiline_passthrough.py
|
|
101
105
|
tests/test_plugin_ai.py
|
|
102
106
|
tests/test_plugin_alias.py
|
|
103
107
|
tests/test_plugin_autopilot.py
|
|
@@ -107,6 +111,8 @@ tests/test_plugin_fetcher.py
|
|
|
107
111
|
tests/test_plugin_help.py
|
|
108
112
|
tests/test_plugin_init.py
|
|
109
113
|
tests/test_plugin_install.py
|
|
114
|
+
tests/test_plugin_install_batch.py
|
|
115
|
+
tests/test_plugin_kssh.py
|
|
110
116
|
tests/test_plugin_lang.py
|
|
111
117
|
tests/test_plugin_mend.py
|
|
112
118
|
tests/test_plugin_portal.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kapsel-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|