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.
Files changed (130) hide show
  1. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/PKG-INFO +1 -1
  2. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/__init__.py +1 -1
  3. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/cli.py +12 -11
  4. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/carapace_installer.py +9 -15
  5. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/add.py +24 -5
  6. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/upgrade.py +59 -4
  7. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/dispatcher.py +11 -0
  8. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/runner.py +103 -8
  9. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/executor.py +11 -1
  10. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/catalog.py +11 -8
  11. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/update_check.py +3 -12
  12. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/prompt.py +6 -16
  13. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/PKG-INFO +1 -1
  14. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/SOURCES.txt +6 -0
  15. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/pyproject.toml +1 -1
  16. kapsel_cli-0.7.2/tests/test_ai_spec_sanitizer.py +144 -0
  17. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_carapace_installer.py +5 -5
  18. kapsel_cli-0.7.2/tests/test_ctrl_b_roaming.py +69 -0
  19. kapsel_cli-0.7.2/tests/test_kp_enhanced.py +111 -0
  20. kapsel_cli-0.7.2/tests/test_multiline_passthrough.py +83 -0
  21. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_ai.py +1 -1
  22. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_alias.py +94 -0
  23. kapsel_cli-0.7.2/tests/test_plugin_install_batch.py +95 -0
  24. kapsel_cli-0.7.2/tests/test_plugin_kssh.py +91 -0
  25. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_preview.py +1 -1
  26. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_upgrade.py +1 -1
  27. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/README.md +0 -0
  28. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/__main__.py +0 -0
  29. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/__init__.py +0 -0
  30. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/carapace_engine.py +0 -0
  31. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/completer.py +0 -0
  32. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/__init__.py +0 -0
  33. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/__init__.py +0 -0
  34. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/completion.py +0 -0
  35. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/config.py +0 -0
  36. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/datadir.py +0 -0
  37. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
  38. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/help.py +0 -0
  39. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/language.py +0 -0
  40. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
  41. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/restart.py +0 -0
  42. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/search.py +0 -0
  43. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/status.py +0 -0
  44. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/builtins/toggle.py +0 -0
  45. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/kps/registry.py +0 -0
  46. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/completion/spec_manager.py +0 -0
  47. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/__init__.py +0 -0
  48. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/__init__.py +0 -0
  49. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/model.py +0 -0
  50. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/block/registry.py +0 -0
  51. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/detector.py +0 -0
  52. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/engine.py +0 -0
  53. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/i18n.py +0 -0
  54. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/__init__.py +0 -0
  55. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/base.py +0 -0
  56. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/context.py +0 -0
  57. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/fetcher.py +0 -0
  58. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/hooks.py +0 -0
  59. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/plugin/manager.py +0 -0
  60. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/tools/__init__.py +0 -0
  61. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/tools/installer.py +0 -0
  62. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/core/tools/registry.py +0 -0
  63. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/i18n.py +0 -0
  64. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  65. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
  66. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/help.yaml +0 -0
  67. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/de/tips.yaml +0 -0
  68. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/en/help.yaml +0 -0
  69. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/en/tips.yaml +0 -0
  70. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  71. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
  72. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/help.yaml +0 -0
  73. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/es/tips.yaml +0 -0
  74. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  75. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
  76. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/help.yaml +0 -0
  77. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/fr/tips.yaml +0 -0
  78. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  79. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
  80. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/help.yaml +0 -0
  81. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ja/tips.yaml +0 -0
  82. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/kapsel.pot +0 -0
  83. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  84. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
  85. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/help.yaml +0 -0
  86. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/ru/tips.yaml +0 -0
  87. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  88. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
  89. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/help.yaml +0 -0
  90. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/locales/zh_CN/tips.yaml +0 -0
  91. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/__init__.py +0 -0
  92. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/config.py +0 -0
  93. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/history.py +0 -0
  94. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/logger.py +0 -0
  95. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/storage/migrate.py +0 -0
  96. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/__init__.py +0 -0
  97. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/banner.py +0 -0
  98. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/card.py +0 -0
  99. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/theme.py +0 -0
  100. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/tips.py +0 -0
  101. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel/ui/viewport.py +0 -0
  102. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/dependency_links.txt +0 -0
  103. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/entry_points.txt +0 -0
  104. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/requires.txt +0 -0
  105. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/kapsel_cli.egg-info/top_level.txt +0 -0
  106. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/setup.cfg +0 -0
  107. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_add_concurrent.py +0 -0
  108. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_block_system.py +0 -0
  109. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_config.py +0 -0
  110. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_dir_nav.py +0 -0
  111. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_history.py +0 -0
  112. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_i18n.py +0 -0
  113. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_autopilot.py +0 -0
  114. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_backup.py +0 -0
  115. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_commit.py +0 -0
  116. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_fetcher.py +0 -0
  117. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_help.py +0 -0
  118. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_init.py +0 -0
  119. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_install.py +0 -0
  120. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_lang.py +0 -0
  121. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_mend.py +0 -0
  122. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_portal.py +0 -0
  123. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_queue.py +0 -0
  124. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_template.py +0 -0
  125. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_plugin_trans.py +0 -0
  126. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_restart_and_pypi.py +0 -0
  127. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_search_and_completions.py +0 -0
  128. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_spec_manager.py +0 -0
  129. {kapsel_cli-0.6.3 → kapsel_cli-0.7.2}/tests/test_toggle.py +0 -0
  130. {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.6.3
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
@@ -3,4 +3,4 @@ Kapsel: Cross-platform adaptive smart terminal capsule.
3
3
  Wrap complexity, expose simplicity.
4
4
  """
5
5
 
6
- __version__ = "0.6.3"
6
+ __version__ = "0.7.2"
@@ -102,17 +102,18 @@ def main(args: Optional[List[str]] = None) -> int:
102
102
  if not stripped:
103
103
  continue
104
104
 
105
- normalized = " ".join(stripped.lower().split())
106
- if normalized in ("exit", "quit", "bye"):
107
- engine.history_mgr.record_command(command=user_input, mode="builtin")
108
- os.environ.pop("KAPSEL_ACTIVE", None)
109
- console.print("[dim]Exited Kapsel.[/]")
110
- break
111
-
112
- if normalized in ("restart", "kapsel restart", "reload"):
113
- engine.history_mgr.record_command(command=user_input, mode="builtin")
114
- from kapsel.completion.kps.builtins.restart import handle_restart
115
- return handle_restart(console=console)
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
- Auto-bootstraps Carapace completion engine on first launch if not already available.
196
- Zero-overhead (0ms) on subsequent launches when already installed.
197
- Gracefully falls back to basic completion if offline, preventing any startup crash.
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("[bold #00f0ff]🎯 First Launch: Auto-initializing Carapace completion engine (1,000+ tools)...[/]")
207
-
208
- try:
209
- success = install_carapace(console=con, force=False)
210
- return success
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
- is_inst, _, _ = _check_plugin_already_installed(t_clean, is_dev=is_dev)
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
- # Check both config.enabled_plugins and actual on-disk installation status with version
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
- is_inst, _, _ = _check_plugin_already_installed(p, is_dev=is_dev)
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
- install_plugin_dependencies(dest_dir, con)
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
- ignoring empty lines and pure comments.
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
- lines = raw_text.strip().splitlines()
29
- result: List[str] = []
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
- cleaned = line.strip()
32
- if not cleaned or cleaned.startswith("#"):
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
- result.append(cleaned)
35
- return result
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
- args = [exe, "/c", cmd]
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
- # 2. Global runtime data directory
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.append(global_in_plugins)
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 cache
271
- global_cache = get_kapsel_dir() / "plugins_catalog.json"
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 cache {global_cache}:[/] {e}")
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 a quick worker thread with a fast join limit
129
- worker = threading.Thread(target=_background_fetch_worker, daemon=True)
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: Always opens Block Navigator / Roaming,
471
- allowing the user to roam across all blocks even if blocks are running in background.
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
- from prompt_toolkit.application.run_in_terminal import in_terminal
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.6.3
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.6.3"
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"