utim-cli 2.3.16__tar.gz → 2.3.18__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.
- {utim_cli-2.3.16 → utim_cli-2.3.18}/CHANGELOG.md +13 -0
- {utim_cli-2.3.16/utim_cli.egg-info → utim_cli-2.3.18}/PKG-INFO +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/pyproject.toml +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim/__init__.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/_version.py +1 -1
- utim_cli-2.3.18/utim_cli/_wizard_smoke_test.py +93 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/bootstrap.py +48 -3
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/client_utils.py +76 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/constants.py +21 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/mcp_client.py +12 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/orchestrator.py +11 -13
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/orchestrator_helpers.py +113 -2
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/orchestrator_llm_transport.py +274 -51
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/orchestrator_tool_exec.py +9 -4
- utim_cli-2.3.18/utim_cli/plugins/loader.py +177 -0
- utim_cli-2.3.18/utim_cli/plugins/manager.py +558 -0
- utim_cli-2.3.18/utim_cli/plugins/runtime.py +175 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/plugins/schema.py +33 -6
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/reflection.py +10 -14
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/auth.py +26 -4
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/concurrency.py +2 -2
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/db.py +14 -11
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/key_rotator.py +807 -43
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/models.py +473 -21
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/router.py +323 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/admin_db_routes.py +549 -549
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/completion_routes.py +459 -297
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/quota_routes.py +44 -10
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/security_routes.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/utils.py +95 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tools.py +164 -4
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/model_dialog.py +93 -14
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/plugins_dialog.py +319 -219
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/thinking_display.py +145 -137
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/update_checker.py +46 -6
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/utim.py +321 -81
- {utim_cli-2.3.16 → utim_cli-2.3.18/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli.egg-info/SOURCES.txt +3 -0
- utim_cli-2.3.16/utim_cli/plugins/manager.py +0 -629
- {utim_cli-2.3.16 → utim_cli-2.3.18}/LICENSE +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/MANIFEST.in +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/README.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/setup.cfg +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/setup.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/config.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/orchestrator_turn_history.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/personalities.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/report.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/share.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/state.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.18}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.18] - 2026-09-02
|
|
4
|
+
|
|
5
|
+
### 🎁 New Free Models Added & Tool Sanitization Fixes
|
|
6
|
+
- Added new free models with key rotation
|
|
7
|
+
- Fixed Google Gemini tool schema sanitization to prevent 400 Bad Request errors
|
|
8
|
+
|
|
9
|
+
## [2.3.17] - 2026-08-31
|
|
10
|
+
|
|
11
|
+
### ✨ Premium Model Visibility for Bonus-Credit Users
|
|
12
|
+
- Free-plan users with bonus credits can now see premium models in the model catalog (server-side `/models/catalog` and client-side TUI gating)
|
|
13
|
+
- Added bonus-credit detection helper with caching and graceful offline fallback
|
|
14
|
+
- Bonus credits are deducted exclusively when premium completions finish; visibility switches back automatically once the balance is exhausted
|
|
15
|
+
|
|
3
16
|
## [2.3.16] - 2026-08-29
|
|
4
17
|
|
|
5
18
|
### 🛠️ Minor Enhancements and Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.18
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: Emend AI Proprietary EULA
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -14,7 +14,7 @@ Quickstart:
|
|
|
14
14
|
try:
|
|
15
15
|
from utim_cli._version import VERSION as __version__
|
|
16
16
|
except ImportError:
|
|
17
|
-
__version__ = "2.3.
|
|
17
|
+
__version__ = "2.3.18"
|
|
18
18
|
|
|
19
19
|
# Developer-friendly core classes (similar to `import genai` / `import openai`)
|
|
20
20
|
from utim_cli.sdk.session import UtimSession as Session
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Smoke test for the new plugin scaffold wizard.
|
|
2
|
+
|
|
3
|
+
Simulates a user filling out the wizard end-to-end and verifies that the
|
|
4
|
+
generated plugin directory + plugin.json contain the basic fields.
|
|
5
|
+
"""
|
|
6
|
+
import os, sys, json, shutil, tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
# Set up an isolated UTIM home before importing the dialog
|
|
11
|
+
TEST_HOME = Path(tempfile.mkdtemp(prefix="utim_wiz_test_"))
|
|
12
|
+
os.environ["UTIM_HOME"] = str(TEST_HOME)
|
|
13
|
+
os.environ["XDG_CONFIG_HOME"] = str(TEST_HOME)
|
|
14
|
+
os.environ["XDG_DATA_HOME"] = str(TEST_HOME)
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, ".")
|
|
17
|
+
from utim_cli.plugins.manager import PluginManager
|
|
18
|
+
from utim_cli.tui.plugins_dialog import _run_plugin_scaffold_wizard
|
|
19
|
+
|
|
20
|
+
mgr = PluginManager()
|
|
21
|
+
# Redirect workspace plugins dir via monkey-patch (property is read-only)
|
|
22
|
+
import utim_cli.plugins.manager as mgr_mod
|
|
23
|
+
real_prop = mgr_mod.PluginManager.workspace_plugins_dir
|
|
24
|
+
fake_workspace_plugins = TEST_HOME / "plugins"
|
|
25
|
+
fake_workspace_plugins.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
def _fake_workspace_dir(self):
|
|
27
|
+
return fake_workspace_plugins
|
|
28
|
+
mgr_mod.PluginManager.workspace_plugins_dir = property(_fake_workspace_dir)
|
|
29
|
+
|
|
30
|
+
# Sequence of inputs the user will type — matches the current 3-step wizard:
|
|
31
|
+
# Step 1: Plugin name
|
|
32
|
+
# Step 2: Description
|
|
33
|
+
# Step 3: Choice (1-4), scope (y/n), confirm (y/n), enable (y/n)
|
|
34
|
+
inputs = iter([
|
|
35
|
+
# Step 1 — Plugin name
|
|
36
|
+
"my-cloud-tools",
|
|
37
|
+
# Step 2 — Description
|
|
38
|
+
"Awesome cloud dev tools",
|
|
39
|
+
# Step 3 — Choice 1 (Skill module)
|
|
40
|
+
"1",
|
|
41
|
+
# Scope: workspace (n = not global)
|
|
42
|
+
"n",
|
|
43
|
+
# Generate now?
|
|
44
|
+
"y",
|
|
45
|
+
# Enable now?
|
|
46
|
+
"y",
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
# Use prompt_toolkit's print_formatted_text + fake_prompter via pt_prompt monkeypatch
|
|
50
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
51
|
+
original = pt_prompt
|
|
52
|
+
def fake_prompt(message="", **kw):
|
|
53
|
+
try:
|
|
54
|
+
v = next(inputs)
|
|
55
|
+
except StopIteration:
|
|
56
|
+
raise EOFError
|
|
57
|
+
return v
|
|
58
|
+
|
|
59
|
+
# Patch the import inside the dialog module
|
|
60
|
+
import utim_cli.tui.plugins_dialog as pd
|
|
61
|
+
with patch.object(pd, "pt_prompt", side_effect=fake_prompt):
|
|
62
|
+
# Also patch the bare `from prompt_toolkit import prompt` reference if any
|
|
63
|
+
try:
|
|
64
|
+
pd._run_plugin_scaffold_wizard(mgr)
|
|
65
|
+
except (StopIteration, EOFError):
|
|
66
|
+
print("WIZARD: ran out of input (likely fine if it finished early)")
|
|
67
|
+
|
|
68
|
+
# Verify the generated plugin
|
|
69
|
+
plugin_dir = mgr.workspace_plugins_dir / "my-cloud-tools"
|
|
70
|
+
print("=" * 60)
|
|
71
|
+
print(f"Plugin dir exists: {plugin_dir.exists()}")
|
|
72
|
+
if plugin_dir.exists():
|
|
73
|
+
print("Files:")
|
|
74
|
+
for p in sorted(plugin_dir.rglob("*")):
|
|
75
|
+
if p.is_file():
|
|
76
|
+
print(f" {p.relative_to(plugin_dir)}")
|
|
77
|
+
print("=" * 60)
|
|
78
|
+
manifest = json.loads((plugin_dir / "plugin.json").read_text(encoding="utf-8"))
|
|
79
|
+
print("plugin.json manifest:")
|
|
80
|
+
print(json.dumps(manifest, indent=2))
|
|
81
|
+
print("=" * 60)
|
|
82
|
+
|
|
83
|
+
# Assertions
|
|
84
|
+
assert manifest["name"] == "my-cloud-tools"
|
|
85
|
+
assert manifest["description"] == "Awesome cloud dev tools"
|
|
86
|
+
assert manifest["author"] == "Developer"
|
|
87
|
+
assert manifest["license"] == "MIT"
|
|
88
|
+
assert len(manifest["skills"]) == 1
|
|
89
|
+
assert manifest["skills"][0] == "skills/my-cloud-tools-workflow/SKILL.md"
|
|
90
|
+
print("✅ ALL ASSERTIONS PASSED")
|
|
91
|
+
else:
|
|
92
|
+
print("❌ Plugin directory was not created")
|
|
93
|
+
sys.exit(1)
|
|
@@ -292,9 +292,54 @@ def scan_available_skills(force_reload: bool = False):
|
|
|
292
292
|
try:
|
|
293
293
|
from utim_cli.plugins.manager import get_plugin_manager
|
|
294
294
|
p_mgr = get_plugin_manager()
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
active_skills_dict = p_mgr.get_active_skills()
|
|
296
|
+
p_items = active_skills_dict.items() if isinstance(active_skills_dict, dict) else [(s.get("name", s.get("raw_name")), s) for s in active_skills_dict]
|
|
297
|
+
for p_skill_name, p_skill_data in p_items:
|
|
298
|
+
if p_skill_name not in disabled:
|
|
299
|
+
s_path = p_skill_data.get("path")
|
|
300
|
+
s_content = p_skill_data.get("content", "")
|
|
301
|
+
s_desc = p_skill_data.get("description", "")
|
|
302
|
+
s_raw_name = p_skill_data.get("raw_name", p_skill_name)
|
|
303
|
+
|
|
304
|
+
if not s_desc and s_content and s_content.startswith("---"):
|
|
305
|
+
parts = s_content.split("---", 2)
|
|
306
|
+
if len(parts) >= 3:
|
|
307
|
+
dm = re.search(r"^description:\s*(.+)$", parts[1], re.MULTILINE)
|
|
308
|
+
if dm:
|
|
309
|
+
s_desc = dm.group(1).strip().strip("\"'")
|
|
310
|
+
|
|
311
|
+
words = set()
|
|
312
|
+
for word in p_skill_name.replace('-', ' ').replace('_', ' ').replace(':', ' ').split():
|
|
313
|
+
if len(word) > 2:
|
|
314
|
+
words.add(word.lower())
|
|
315
|
+
for word in s_raw_name.replace('-', ' ').replace('_', ' ').split():
|
|
316
|
+
if len(word) > 2:
|
|
317
|
+
words.add(word.lower())
|
|
318
|
+
for word in s_desc.replace(',', ' ').replace('.', ' ').replace('(', ' ').replace(')', ' ').split():
|
|
319
|
+
w = word.lower().strip()
|
|
320
|
+
if len(w) > 2 and w not in stop_words:
|
|
321
|
+
words.add(w)
|
|
322
|
+
|
|
323
|
+
size_bytes = 0
|
|
324
|
+
try:
|
|
325
|
+
if hasattr(s_path, "stat"):
|
|
326
|
+
size_bytes = s_path.stat().st_size
|
|
327
|
+
except Exception:
|
|
328
|
+
size_bytes = len(s_content.encode("utf-8")) if s_content else 0
|
|
329
|
+
|
|
330
|
+
skill_entry = {
|
|
331
|
+
"path": s_path,
|
|
332
|
+
"name": p_skill_name,
|
|
333
|
+
"folder_name": s_raw_name,
|
|
334
|
+
"frontmatter_name": s_raw_name,
|
|
335
|
+
"description": s_desc,
|
|
336
|
+
"keywords": list(words),
|
|
337
|
+
"content": s_content,
|
|
338
|
+
"size_bytes": size_bytes,
|
|
339
|
+
}
|
|
340
|
+
skills[p_skill_name] = skill_entry
|
|
341
|
+
if s_raw_name not in skills and s_raw_name not in disabled:
|
|
342
|
+
skills[s_raw_name] = skill_entry
|
|
298
343
|
except Exception:
|
|
299
344
|
pass
|
|
300
345
|
|
|
@@ -572,6 +572,82 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
572
572
|
return requests.post("https://openrouter.ai/api/v1/chat/completions", json=json_data, headers=headers, stream=stream or json_data.get("stream"), timeout=timeout, verify=config.verify_ssl)
|
|
573
573
|
|
|
574
574
|
|
|
575
|
+
def sanitize_tools_for_api(tools: Optional[List[Dict[str, Any]]], model_id: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
|
576
|
+
"""Sanitize and validate JSON schemas in tool declarations.
|
|
577
|
+
|
|
578
|
+
Removes unsupported JSON schema keywords (like $schema, additionalProperties,
|
|
579
|
+
propertyNames, patternProperties, definitions), ensures arrays have valid 'items' schemas,
|
|
580
|
+
and converts uppercase / invalid type declarations that cause Google Gemini / GLM / OpenAPI
|
|
581
|
+
validators to reject payloads with 400 'Invalid JSON payload' errors.
|
|
582
|
+
"""
|
|
583
|
+
if not tools:
|
|
584
|
+
return tools
|
|
585
|
+
|
|
586
|
+
TYPE_MAP = {
|
|
587
|
+
"STRING": "string",
|
|
588
|
+
"NUMBER": "number",
|
|
589
|
+
"INTEGER": "integer",
|
|
590
|
+
"BOOLEAN": "boolean",
|
|
591
|
+
"ARRAY": "array",
|
|
592
|
+
"OBJECT": "object",
|
|
593
|
+
"NULL": "null"
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
def _clean_schema_node(node):
|
|
597
|
+
if not isinstance(node, dict):
|
|
598
|
+
return node
|
|
599
|
+
cleaned = {}
|
|
600
|
+
disallowed_keys = {
|
|
601
|
+
"$schema", "$id", "definitions", "$defs", "propertyNames",
|
|
602
|
+
"patternProperties", "additionalProperties", "unevaluatedProperties",
|
|
603
|
+
}
|
|
604
|
+
for k, v in node.items():
|
|
605
|
+
if k in disallowed_keys:
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
# Standardize JSON schema type names to lowercase
|
|
609
|
+
if k == "type":
|
|
610
|
+
if isinstance(v, str):
|
|
611
|
+
v = TYPE_MAP.get(v, v.lower())
|
|
612
|
+
elif isinstance(v, list):
|
|
613
|
+
v = [TYPE_MAP.get(item, item.lower()) if isinstance(item, str) else item for item in v]
|
|
614
|
+
|
|
615
|
+
if isinstance(v, dict):
|
|
616
|
+
cleaned[k] = _clean_schema_node(v)
|
|
617
|
+
elif isinstance(v, list):
|
|
618
|
+
cleaned[k] = [_clean_schema_node(item) if isinstance(item, dict) else item for item in v]
|
|
619
|
+
else:
|
|
620
|
+
cleaned[k] = v
|
|
621
|
+
|
|
622
|
+
# Strict validator fix: If type is 'array' and 'items' is missing or empty dict, default items to string or object
|
|
623
|
+
if cleaned.get("type") == "array":
|
|
624
|
+
items = cleaned.get("items")
|
|
625
|
+
if not items or items == {}:
|
|
626
|
+
cleaned["items"] = {"type": "string"}
|
|
627
|
+
elif isinstance(items, dict):
|
|
628
|
+
cleaned["items"] = _clean_schema_node(items)
|
|
629
|
+
|
|
630
|
+
# Strict validator fix: If type is 'object', ensure properties is a dict if present
|
|
631
|
+
if cleaned.get("type") == "object":
|
|
632
|
+
if "properties" in cleaned and not isinstance(cleaned["properties"], dict):
|
|
633
|
+
cleaned["properties"] = {}
|
|
634
|
+
|
|
635
|
+
return cleaned
|
|
636
|
+
|
|
637
|
+
sanitized_tools = []
|
|
638
|
+
for tool in tools:
|
|
639
|
+
if not isinstance(tool, dict):
|
|
640
|
+
continue
|
|
641
|
+
tool_copy = dict(tool)
|
|
642
|
+
if "function" in tool_copy and isinstance(tool_copy["function"], dict):
|
|
643
|
+
fn_copy = dict(tool_copy["function"])
|
|
644
|
+
if "parameters" in fn_copy and isinstance(fn_copy["parameters"], dict):
|
|
645
|
+
fn_copy["parameters"] = _clean_schema_node(fn_copy["parameters"])
|
|
646
|
+
tool_copy["function"] = fn_copy
|
|
647
|
+
sanitized_tools.append(tool_copy)
|
|
648
|
+
return sanitized_tools
|
|
649
|
+
|
|
650
|
+
|
|
575
651
|
def sanitize_messages_for_api(messages, model_id: Optional[str] = None):
|
|
576
652
|
"""Normalize conversation history into a provider-safe shape before sending.
|
|
577
653
|
|
|
@@ -11,7 +11,26 @@ SUBAGENT_DEFAULTS = {
|
|
|
11
11
|
"analyze_image": "google/gemma-4-31b-it:free",
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
APINEX_MODEL_IDS = {
|
|
15
|
+
"claude/opus-5",
|
|
16
|
+
"claude/sonnet-5",
|
|
17
|
+
"deepseek/v4-flash-0731",
|
|
18
|
+
"deepseek/v4-pro-0813",
|
|
19
|
+
"free/deepseek-v4-flash-0731",
|
|
20
|
+
"free/deepseek-v4-pro-0813",
|
|
21
|
+
"free/glm-5.3-flash",
|
|
22
|
+
"free/gemini-3.1-pro",
|
|
23
|
+
"free/gemini-3.7-flash",
|
|
24
|
+
"gemini/3.1-pro",
|
|
25
|
+
"gemini/3.7-flash",
|
|
26
|
+
"kimi/k3",
|
|
27
|
+
"gpt/5.6-luna",
|
|
28
|
+
"gpt/5.6-sol",
|
|
29
|
+
"gpt/5.6-terra",
|
|
30
|
+
"glm/5.3",
|
|
31
|
+
"glm/5.3-flash",
|
|
32
|
+
"grok/4.6",
|
|
33
|
+
}
|
|
15
34
|
|
|
16
35
|
|
|
17
36
|
import sys
|
|
@@ -19,3 +38,4 @@ _IS_LEGACY_WIN = (sys.platform == "win32" and "WT_SESSION" not in os.environ)
|
|
|
19
38
|
PROMPT_SYMBOL = ">" if _IS_LEGACY_WIN else "❯"
|
|
20
39
|
ARROW_SYMBOL = "->" if _IS_LEGACY_WIN else "➔"
|
|
21
40
|
|
|
41
|
+
|
|
@@ -213,7 +213,18 @@ class MCPManager:
|
|
|
213
213
|
servers = config.get("mcpServers") or {}
|
|
214
214
|
if not isinstance(servers, dict):
|
|
215
215
|
servers = {}
|
|
216
|
-
|
|
216
|
+
|
|
217
|
+
# Merge active MCP servers declared by enabled plugins
|
|
218
|
+
try:
|
|
219
|
+
from utim_cli.plugins.manager import get_plugin_manager
|
|
220
|
+
plugin_mcp = get_plugin_manager().get_active_mcp_servers()
|
|
221
|
+
if isinstance(plugin_mcp, dict):
|
|
222
|
+
for srv_name, srv_conf in plugin_mcp.items():
|
|
223
|
+
if srv_name not in servers:
|
|
224
|
+
servers[srv_name] = srv_conf
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
217
228
|
for name, srv_config in servers.items():
|
|
218
229
|
command = srv_config.get("command")
|
|
219
230
|
args = srv_config.get("args", [])
|
|
@@ -99,25 +99,23 @@ from .orchestrator_helpers import ( # noqa: F401,E402
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
class _ServerUnavailableError(RuntimeError):
|
|
103
|
-
"""Raised when the UTIM server cannot be reached and no local key is configured.
|
|
104
|
-
Caught in run_task to display a clean user-facing message (no traceback).
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class _FatalClientError(RuntimeError):
|
|
109
|
-
"""Raised when a non-retryable gating or credit validation error occurs.
|
|
110
|
-
Caught in run_task to immediately abort the turn without retry.
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
102
|
from .orchestrator_tool_exec import ToolExecutionMixin # noqa: E402
|
|
115
103
|
|
|
116
104
|
|
|
117
105
|
from .orchestrator_turn_history import TurnHistoryMixin # noqa: E402
|
|
118
106
|
|
|
119
107
|
|
|
120
|
-
from .orchestrator_llm_transport import
|
|
108
|
+
from .orchestrator_llm_transport import ( # noqa: E402,F401
|
|
109
|
+
LLMTransportMixin,
|
|
110
|
+
# ── IMPORTANT: bind the transport's exception classes here ──
|
|
111
|
+
# The transport raises ITS OWN _ServerUnavailableError/_FatalClientError.
|
|
112
|
+
# Previously orchestrator.py defined duplicate same-named classes, so
|
|
113
|
+
# `except _FatalClientError` never caught transport-raised errors and
|
|
114
|
+
# quota/rate-limit failures fell into the generic transient-retry loop
|
|
115
|
+
# ("Transient error on iteration 1 … Retrying in 3s…" x3 then abort).
|
|
116
|
+
_ServerUnavailableError,
|
|
117
|
+
_FatalClientError,
|
|
118
|
+
)
|
|
121
119
|
|
|
122
120
|
|
|
123
121
|
class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
@@ -269,6 +269,57 @@ def get_cached_quota(api_key: str, force_refresh: bool = False) -> Optional[Dict
|
|
|
269
269
|
return _QUOTA_CACHE.get("data")
|
|
270
270
|
|
|
271
271
|
|
|
272
|
+
def format_quota_reset_countdown(reset_at_iso: Optional[str]) -> Tuple[str, str]:
|
|
273
|
+
"""Parse ISO reset timestamp and return (countdown_str, human_time_str).
|
|
274
|
+
|
|
275
|
+
countdown_str format: '1h:45m', '0h:32m', etc.
|
|
276
|
+
human_time_str format: '2026-09-02 02:45 UTC' or 'momentarily'
|
|
277
|
+
"""
|
|
278
|
+
if not reset_at_iso:
|
|
279
|
+
return "in a few hours", "next refill window"
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
from datetime import datetime, timezone
|
|
283
|
+
clean_iso = str(reset_at_iso).strip()
|
|
284
|
+
if clean_iso.endswith("+00:00Z"):
|
|
285
|
+
clean_iso = clean_iso[:-1]
|
|
286
|
+
elif clean_iso.endswith("Z"):
|
|
287
|
+
if "+" not in clean_iso and "-" not in clean_iso[10:]:
|
|
288
|
+
clean_iso = clean_iso[:-1] + "+00:00"
|
|
289
|
+
else:
|
|
290
|
+
clean_iso = clean_iso[:-1]
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
reset_dt = datetime.fromisoformat(clean_iso)
|
|
294
|
+
except ValueError:
|
|
295
|
+
clean_iso_alt = str(reset_at_iso).strip().replace("Z", "+00:00")
|
|
296
|
+
reset_dt = datetime.fromisoformat(clean_iso_alt)
|
|
297
|
+
|
|
298
|
+
now_dt = datetime.now(timezone.utc)
|
|
299
|
+
if reset_dt.tzinfo is None:
|
|
300
|
+
reset_dt = reset_dt.replace(tzinfo=timezone.utc)
|
|
301
|
+
|
|
302
|
+
diff = reset_dt - now_dt
|
|
303
|
+
total_seconds = max(0, int(round(diff.total_seconds())))
|
|
304
|
+
if total_seconds <= 30:
|
|
305
|
+
return "0h:01m", "momentarily"
|
|
306
|
+
|
|
307
|
+
total_minutes = int(round(total_seconds / 60.0))
|
|
308
|
+
hours = total_minutes // 60
|
|
309
|
+
minutes = total_minutes % 60
|
|
310
|
+
countdown = f"{hours}h:{minutes:02d}m" if hours > 0 else f"0h:{minutes:02d}m"
|
|
311
|
+
human_time = reset_dt.strftime("%Y-%m-%d %H:%M UTC")
|
|
312
|
+
return countdown, human_time
|
|
313
|
+
except Exception:
|
|
314
|
+
return "in a few hours", str(reset_at_iso)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def invalidate_quota_cache() -> None:
|
|
318
|
+
"""Invalidate in-memory quota cache to force a fresh server check."""
|
|
319
|
+
global _QUOTA_CACHE
|
|
320
|
+
_QUOTA_CACHE["timestamp"] = 0
|
|
321
|
+
|
|
322
|
+
|
|
272
323
|
from utim_cli.config import get_utim_dir
|
|
273
324
|
_utim_dir_posix = get_utim_dir().as_posix()
|
|
274
325
|
|
|
@@ -1252,7 +1303,7 @@ def get_frozen_system_prompt(user_prompt: str = "", current_iteration: int = 0,
|
|
|
1252
1303
|
for pr in active_plugin_rules:
|
|
1253
1304
|
p_content = pr.get("content", "").strip()
|
|
1254
1305
|
p_name = pr.get("name", "plugin_rule")
|
|
1255
|
-
p_plugin = pr.get("plugin_name"
|
|
1306
|
+
p_plugin = pr.get("plugin_name") or pr.get("plugin") or "plugin"
|
|
1256
1307
|
if p_content:
|
|
1257
1308
|
plugin_rules_blocks.append(f"#### Plugin Rule [{p_plugin} / {p_name}]\n{p_content}")
|
|
1258
1309
|
if plugin_rules_blocks:
|
|
@@ -1354,6 +1405,66 @@ def get_frozen_system_prompt(user_prompt: str = "", current_iteration: int = 0,
|
|
|
1354
1405
|
except Exception:
|
|
1355
1406
|
pass
|
|
1356
1407
|
|
|
1408
|
+
plugins_prompt = ""
|
|
1409
|
+
try:
|
|
1410
|
+
from utim_cli.plugins.manager import get_plugin_manager
|
|
1411
|
+
from utim_cli.config import get_utim_dir
|
|
1412
|
+
p_mgr = get_plugin_manager()
|
|
1413
|
+
discovered_plugins = p_mgr.discover_plugins()
|
|
1414
|
+
global_plugins_path = str((get_utim_dir() / "plugins").resolve()).replace("\\", "/")
|
|
1415
|
+
workspace_plugins_path = str((p_mgr.workspace_plugins_dir).resolve()).replace("\\", "/")
|
|
1416
|
+
|
|
1417
|
+
plugin_lines = []
|
|
1418
|
+
for pname, p in sorted(discovered_plugins.items(), key=lambda x: x[0]):
|
|
1419
|
+
status = "active (enabled)" if p.enabled and p.is_valid else ("disabled" if not p.enabled else "invalid")
|
|
1420
|
+
p_path_str = str(p.path.resolve()).replace("\\", "/")
|
|
1421
|
+
counts = p.get_summary_counts()
|
|
1422
|
+
details = []
|
|
1423
|
+
if counts.get("skills"):
|
|
1424
|
+
skill_names = [s.get("raw_name", s.get("name")) for s in p.get_skills()]
|
|
1425
|
+
details.append(f"skills: {', '.join(skill_names)}")
|
|
1426
|
+
if counts.get("rules"):
|
|
1427
|
+
rule_names = [r.get("name") for r in p.get_rules()]
|
|
1428
|
+
details.append(f"rules: {', '.join(rule_names)}")
|
|
1429
|
+
if counts.get("tools"):
|
|
1430
|
+
tool_names = [t.get("name") for t in p.get_tools()]
|
|
1431
|
+
details.append(f"native tools: {', '.join(tool_names)}")
|
|
1432
|
+
if counts.get("mcp"):
|
|
1433
|
+
mcp_names = list(p.get_mcp_servers().keys())
|
|
1434
|
+
details.append(f"mcp servers: {', '.join(mcp_names)}")
|
|
1435
|
+
if counts.get("hooks"):
|
|
1436
|
+
hook_names = list(p.get_hooks().keys())
|
|
1437
|
+
details.append(f"hooks: {', '.join(hook_names)}")
|
|
1438
|
+
|
|
1439
|
+
detail_str = f" [{'; '.join(details)}]" if details else ""
|
|
1440
|
+
desc = f" — {p.description}" if p.description else ""
|
|
1441
|
+
plugin_lines.append(f"- **{p.name}** (v{p.version}, {p.source}, status: {status}, path: file:///{p_path_str}){desc}{detail_str}")
|
|
1442
|
+
|
|
1443
|
+
if plugin_lines:
|
|
1444
|
+
plugins_prompt = (
|
|
1445
|
+
f"\n\n### INSTALLED & ACTIVE PLUGINS ###\n"
|
|
1446
|
+
f"Global Plugins Directory: file:///{global_plugins_path}\n"
|
|
1447
|
+
f"Workspace Plugins Directory: file:///{workspace_plugins_path}\n\n"
|
|
1448
|
+
f"The following plugins are registered in the environment:\n"
|
|
1449
|
+
+ "\n".join(plugin_lines) + "\n\n"
|
|
1450
|
+
f"Plugin capabilities:\n"
|
|
1451
|
+
f"- **Skills**: Specialized instructions matching tasks; read their SKILL.md via `read_file` when relevant.\n"
|
|
1452
|
+
f"- **Rules**: Plugin guidelines and coding standards automatically applied to code generation.\n"
|
|
1453
|
+
f"- **Tools**: Native callable python/shell tools namespaced as `<plugin>__<tool>`.\n"
|
|
1454
|
+
f"- **MCP Servers**: Model Context Protocol servers automatically started to provide external tools.\n"
|
|
1455
|
+
f"- **Hooks**: Lifecycle hooks triggered on session/agent events.\n"
|
|
1456
|
+
f"- Users can manage plugins via `/plugins` interactive menu or CLI (`/plugin list`, `/plugin info <name>`, `/plugin enable <name>`, `/plugin disable <name>`, `/plugin install <name/url>`, `/plugin create <name>`).\n"
|
|
1457
|
+
)
|
|
1458
|
+
else:
|
|
1459
|
+
plugins_prompt = (
|
|
1460
|
+
f"\n\n### PLUGINS DIRECTORY & CONFIGURATION ###\n"
|
|
1461
|
+
f"Global Plugins Directory: file:///{global_plugins_path}\n"
|
|
1462
|
+
f"Workspace Plugins Directory: file:///{workspace_plugins_path}\n"
|
|
1463
|
+
f"Plugins extend UTIM with bundled skills, rules, tools, MCP servers, and hooks. Use `/plugins` to browse and install plugins.\n"
|
|
1464
|
+
)
|
|
1465
|
+
except Exception:
|
|
1466
|
+
pass
|
|
1467
|
+
|
|
1357
1468
|
session_summary = ""
|
|
1358
1469
|
import os
|
|
1359
1470
|
from utim_cli.config import get_utim_dir
|
|
@@ -1392,7 +1503,7 @@ def get_frozen_system_prompt(user_prompt: str = "", current_iteration: int = 0,
|
|
|
1392
1503
|
except Exception:
|
|
1393
1504
|
personality_prompt = ""
|
|
1394
1505
|
|
|
1395
|
-
return base_prompt + rules_prompt + mcp_prompt + skills_prompt + session_prompt + personality_prompt
|
|
1506
|
+
return base_prompt + rules_prompt + mcp_prompt + plugins_prompt + skills_prompt + session_prompt + personality_prompt
|
|
1396
1507
|
|
|
1397
1508
|
|
|
1398
1509
|
def get_live_context_block(user_prompt: str = "", messages: Optional[List[Dict]] = None) -> str:
|