mlspace-plugin 0.3.0__tar.gz → 0.3.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.
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/INSTALL.md +25 -4
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/INSTALL_AGENT.md +11 -2
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/PKG-INFO +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/RELEASE.md +5 -5
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/.claude-plugin/plugin.json +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/.codex-plugin/plugin.json +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/.mcp.json +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/mcp.json +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/plugin.json +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/pyproject.toml +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/__init__.py +2 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/client_setup.py +56 -7
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/init_cli.py +15 -6
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/server.py +5 -1
- mlspace_plugin-0.3.2/src/mlspace_mcp/setup_conflicts.py +105 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/setup_verification.py +15 -4
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_client_setup.py +1 -1
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_onboarding_init.py +57 -1
- mlspace_plugin-0.3.2/tests/test_opencode_journey.py +249 -0
- mlspace_plugin-0.3.2/tests/test_setup_conflicts.py +202 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_setup_native_journey.py +2 -1
- mlspace_plugin-0.3.2/tests/test_user_agent.py +59 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/.agents/plugins/marketplace.json +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/.claude-plugin/marketplace.json +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/.env.example +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/.gitignore +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/AGENTS.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/CONTRIBUTING.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/LICENSE +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/README.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/README.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-call-deployed-model-predict/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-deploy-inference-service/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-diagnose-data-transfer/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-diagnose-failed-training-job/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-diagnose-job-launch-rejection/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-diagnose-unhealthy-inference/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-discover-launchable-resources/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-launch-training-job/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-list-running-jobs/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-resume-notebook-and-set-autoshutdown/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-stage-s3-to-nfs/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/plugin/skills/mlspace-transfer-compute-node/SKILL.md +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/build_plugin.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/demo_multi_workspace.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/gen_body_schemas.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/gen_plugin.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/mcp_call.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/serve_stdio.sh +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/scripts/serve_stdio_dryrun.sh +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/__main__.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/_status.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/auth.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/bodyspec.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/client.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/config.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/errors.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/formatting.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/job_hooks.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/jsonc.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/native_plugins.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/overview.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/registry.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/resource_refs.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/setup_ui.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/spec/body_schemas.json +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/spec/openapi.json +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tls.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/__init__.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/allocations.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/async_inference.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/build_image.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/dalle.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/data_transfer.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/docker_registry.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/inference.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/jobs.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/notebooks.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/queues.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/resources.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/tensorboards.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/tools/workspaces.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/src/mlspace_mcp/workspace_context.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/conftest.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/specutil.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_allocations.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_artifact_sync.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_async_inference.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_auth.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_autofill.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_bodyspec.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_build_image.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_client.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_configs_shrink.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_credential_pointer.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_dalle.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_data_transfer.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_docker_registry.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_drift.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_dry_run.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_dry_run_fidelity.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_env_file.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_errors.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_false_success.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_formatting.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_init_cli.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_integration.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_job_status.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_jobs.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_log_normalisation.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_mcp_error_contract.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_native_cli_integration.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_native_plugins.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_notebooks.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_onboarding_tls.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_overview.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_plugin_bundle.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_plugin_cli.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_plugin_release.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_preflight.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_queue_scope.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_queues.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_registry.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_resource_refs.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_resources.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_route_confusion.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_setup_ui.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_setup_verification.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_skills.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_tensorboards.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_workspace_context.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_workspaces.py +0 -0
- {mlspace_plugin-0.3.0 → mlspace_plugin-0.3.2}/tests/test_wrapper_hardening.py +0 -0
|
@@ -21,7 +21,7 @@ mlspace-plugin setup
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Если пакет уже установлен: `uv tool install --force .`.
|
|
24
|
-
При установке из wheel: `uv tool install /путь/к/mlspace_plugin-0.3.
|
|
24
|
+
При установке из wheel: `uv tool install /путь/к/mlspace_plugin-0.3.2-py3-none-any.whl`.
|
|
25
25
|
Не используйте editable-установку из `/tmp` для постоянной работы.
|
|
26
26
|
Далее команды показаны для PyPI. После установки исходников или wheel через
|
|
27
27
|
`uv tool install` заменяйте `uvx mlspace-plugin@latest` на `mlspace-plugin`,
|
|
@@ -84,8 +84,27 @@ uvx mlspace-plugin@latest setup --force --client-path opencode=/полный/п
|
|
|
84
84
|
`--force` повторяет настройку с сохранёнными ключами на прежнем endpoint.
|
|
85
85
|
`--replace-credentials` запрашивает новые. При смене endpoint ключи запрашиваются
|
|
86
86
|
заново, если источник явно не выбран. `--client` можно повторять.
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
Посторонние MCP, комментарии JSONC и дополнительные пользовательские скиллы сохраняются.
|
|
88
|
+
Если в OpenCode уже есть другое подключение `mcp.mlspace` или отличающиеся скиллы
|
|
89
|
+
с именами из пакета, мастер **до ввода ключей** покажет все эти конфликты вместе.
|
|
90
|
+
Для подключения выберите `r`, чтобы сохранить резервную копию конфига и заменить
|
|
91
|
+
только `mcp.mlspace`. Для каждого конфликтующего скилла доступны:
|
|
92
|
+
|
|
93
|
+
- `k` — оставить свою версию активной и исключить её из обновлений плагина.
|
|
94
|
+
Этот выбор сохраняется для последующих запусков setup. Проверка сообщит,
|
|
95
|
+
что пользовательская версия не проверялась на совпадение с релизом.
|
|
96
|
+
- `r` — сохранить весь каталог скилла, включая вспомогательные файлы, в резервной
|
|
97
|
+
копии и установить пакетный `SKILL.md`.
|
|
98
|
+
- Enter — отменить настройку без замены конфликтующих файлов.
|
|
99
|
+
|
|
100
|
+
Резервные копии находятся в `backups/setup-*` рядом с `install-state.json`
|
|
101
|
+
в каталоге настроек MLSpace, **вне каталогов скиллов**; мастер печатает точный путь.
|
|
102
|
+
Они создаются перед заменой после проверки credentials. Если резервное копирование
|
|
103
|
+
не удалось, конфликтующие файлы не заменяются. Отличие от пакета само по себе
|
|
104
|
+
не означает ручное редактирование: это может быть файл от старой установки.
|
|
105
|
+
`--force` не разрешает замену конфликтов. В `--non-interactive` мастер перечисляет
|
|
106
|
+
их и останавливается; для выбора запустите ту же команду в своём терминале без этого флага.
|
|
107
|
+
|
|
89
108
|
Уже установленный `mlspace@mlspace` из этого репозитория переиспользуется.
|
|
90
109
|
Если имя marketplace занято другим источником или есть отдельная прямая регистрация
|
|
91
110
|
MLSpace в Claude/Codex, мастер остановится до ввода ключей. Разберите указанное
|
|
@@ -126,7 +145,9 @@ uvx mlspace-plugin@latest setup --force
|
|
|
126
145
|
|
|
127
146
|
Claude Code и Codex обновляют нативный плагин через свой менеджер; MCP и скиллы
|
|
128
147
|
приходят из одного релиза. OpenCode обновляет команду MCP и свои скиллы.
|
|
129
|
-
|
|
148
|
+
Для отличающихся конфигов и скиллов OpenCode мастер предлагает выбор, описанный выше.
|
|
149
|
+
Дополнительные скиллы с отдельными именами, в том числе `mlspace-*`, не мешают
|
|
150
|
+
установке и проверке. Скиллы, оставленные через `k`, не обновляются плагином.
|
|
130
151
|
Кеш нативного плагина принадлежит менеджеру клиента: свои правки храните отдельно.
|
|
131
152
|
|
|
132
153
|
Для постоянной установки сначала `uv tool upgrade mlspace-plugin`, затем
|
|
@@ -30,8 +30,17 @@
|
|
|
30
30
|
Попросите открыть новую сессию клиента (при необходимости перезапустить).
|
|
31
31
|
После появления MCP выполните read-only запрос списка выбранных workspace.
|
|
32
32
|
До этого не заявляйте, что клиент уже загрузил MCP и скиллы.
|
|
33
|
-
6.
|
|
34
|
-
|
|
33
|
+
6. При существующей настройке объясните выбор до отправки человека в терминал:
|
|
34
|
+
мастер покажет конфликты OpenCode одним списком перед вводом ключей.
|
|
35
|
+
`k` оставляет пользовательский скилл активным и исключает из обновлений;
|
|
36
|
+
`r` сохраняет резервную копию и заменяет конфликтующий файл; Enter отменяет.
|
|
37
|
+
Новые скиллы пользователя с отдельными именами сохраняются автоматически.
|
|
38
|
+
Не удаляйте каталоги `mlspace-*` и не считайте отличие от пакета доказательством
|
|
39
|
+
ручного изменения. Не заменяйте выбор человека флагом `--force` — он этого не делает.
|
|
40
|
+
В неинтерактивном режиме конфликты перечисляются без изменений: предложите человеку
|
|
41
|
+
запустить ту же команду без `--non-interactive`, сохранив endpoint, путь и выбор клиентов.
|
|
42
|
+
Для конфликта нативного плагина Claude/Codex разберите указанный несекретный путь;
|
|
43
|
+
не удаляйте чужую интеграцию автоматически.
|
|
35
44
|
При частичном успехе сообщите, какие этапы завершены, и используйте команду `Retry`
|
|
36
45
|
из ошибки: она сохраняет выбор незавершённых подключений. Для автоматизации
|
|
37
46
|
добавьте прежние `--non-interactive` и `--workspace`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mlspace-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Cloud.ru MLSpace plugin: MCP server, agent skills and setup
|
|
5
5
|
Project-URL: Repository, https://github.com/noesskeetit/mlspace-plugin
|
|
6
6
|
Project-URL: Issues, https://github.com/noesskeetit/mlspace-plugin/issues
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
`uv lock` и `uv run python scripts/gen_plugin.py`. Обновите примеры имён wheel/ZIP
|
|
8
8
|
в инструкциях, если версия присутствует в имени файла.
|
|
9
9
|
2. Выполните проверки из `CONTRIBUTING.md` на Linux и macOS.
|
|
10
|
-
3. Соберите пакет (в командах ниже замените 0.3.
|
|
10
|
+
3. Соберите пакет (в командах ниже замените 0.3.2 на выпускаемую версию):
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
uv build --out-dir dist/release-0.3.
|
|
14
|
-
uv run python scripts/build_plugin.py --output dist/plugin-release-0.3.
|
|
13
|
+
uv build --out-dir dist/release-0.3.2
|
|
14
|
+
uv run python scripts/build_plugin.py --output dist/plugin-release-0.3.2
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
4. Просмотрите содержимое wheel, sdist и ZIP. В них не должно быть credentials,
|
|
@@ -23,11 +23,11 @@ uv run python scripts/build_plugin.py --output dist/plugin-release-0.3.0
|
|
|
23
23
|
`UV_PUBLISH_TOKEN` средствами вашего хранилища секретов, не аргументом команды:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
uv publish --trusted-publishing never dist/release-0.3.
|
|
26
|
+
uv publish --trusted-publishing never dist/release-0.3.2/mlspace_plugin-0.3.2-py3-none-any.whl dist/release-0.3.2/mlspace_plugin-0.3.2.tar.gz
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
6. Проверьте версию и SHA256 обоих файлов через PyPI JSON API; проверьте установку
|
|
30
|
-
`uvx --from mlspace-plugin==0.3.
|
|
30
|
+
`uvx --from mlspace-plugin==0.3.2 mlspace-plugin --version` из нового кеша.
|
|
31
31
|
7. Только после проверки точной версии в PyPI обновите рабочую ветку GitHub
|
|
32
32
|
marketplace и создайте тег релиза. До этого проверяйте изменения на отдельной
|
|
33
33
|
release-ветке или локальном Git marketplace: main не должен ссылаться на
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "mlspace",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"description": "Cloud.ru MLSpace: training jobs, inference, notebooks, data transfer, registry and compute catalog — an MCP server plus skills.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Cloud.ru"
|
|
@@ -18,6 +18,7 @@ from .config import credential_pointer_file
|
|
|
18
18
|
from .jsonc import loads as load_jsonc
|
|
19
19
|
from .jsonc import set_member as set_jsonc_member
|
|
20
20
|
from .native_plugins import inspect_native, install_native
|
|
21
|
+
from .setup_conflicts import Conflict, Resolutions, backup_replacements, conflict_message
|
|
21
22
|
|
|
22
23
|
CLIENTS = {'claude-code': 'claude', 'codex': 'codex', 'opencode': 'opencode'}
|
|
23
24
|
|
|
@@ -142,13 +143,36 @@ def managed_clients(overrides: dict[str, str] | None = None) -> dict[str, str]:
|
|
|
142
143
|
return result
|
|
143
144
|
|
|
144
145
|
|
|
145
|
-
def prepare_clients(clients: dict[str, str], env_file: Path, command: list[str]
|
|
146
|
+
def prepare_clients(clients: dict[str, str], env_file: Path, command: list[str], *,
|
|
147
|
+
collect_conflicts: bool = False, resolutions: Resolutions | None = None) -> dict[str, Any]:
|
|
146
148
|
"""Check client capabilities and conflicts before setup saves credentials."""
|
|
147
149
|
state_path = install_state_file()
|
|
148
150
|
state = json.loads(state_path.read_text()) if state_path.exists() else {}
|
|
149
151
|
clients = managed_clients(clients) | clients
|
|
150
152
|
sources = _skill_sources()
|
|
151
153
|
writes: dict[Path, str] = {}
|
|
154
|
+
conflicts: list[Conflict] = []
|
|
155
|
+
replacements: list[Conflict] = []
|
|
156
|
+
overrides = set(state.get('skill_overrides', []))
|
|
157
|
+
decisions = resolutions or {}
|
|
158
|
+
examined: set[str] = set()
|
|
159
|
+
|
|
160
|
+
def resolve(path: Path, kind: str) -> str:
|
|
161
|
+
item = Conflict.inspect(path, kind)
|
|
162
|
+
decision = decisions.get(str(path))
|
|
163
|
+
if decision is None:
|
|
164
|
+
conflicts.append(item)
|
|
165
|
+
return ''
|
|
166
|
+
action, approved = decision
|
|
167
|
+
examined.add(str(path))
|
|
168
|
+
if item.fingerprint != approved:
|
|
169
|
+
raise ValueError(f'File changed after your choice; run setup again: {path}')
|
|
170
|
+
if action == 'replace':
|
|
171
|
+
replacements.append(item)
|
|
172
|
+
elif action != 'keep' or kind != 'skill':
|
|
173
|
+
raise ValueError(f'Invalid conflict choice for {path}')
|
|
174
|
+
return action
|
|
175
|
+
|
|
152
176
|
registrations: list[tuple[str, str, Path, dict[str, Any] | None, Any]] = []
|
|
153
177
|
for client, executable in clients.items():
|
|
154
178
|
if client not in CLIENTS:
|
|
@@ -179,27 +203,47 @@ def prepare_clients(clients: dict[str, str], env_file: Path, command: list[str])
|
|
|
179
203
|
comparable = dict(current)
|
|
180
204
|
comparable.pop('type', None) if client != 'opencode' else None
|
|
181
205
|
if comparable != entry and _hash(json.dumps(current, sort_keys=True)) != previous:
|
|
182
|
-
|
|
206
|
+
resolve(config, 'connection')
|
|
183
207
|
registrations.append((client, executable, config, entry, current))
|
|
184
208
|
skills_root = Path(os.environ.get('XDG_CONFIG_HOME', str(Path.home() / '.config'))) / 'opencode/skills'
|
|
185
209
|
for name, content in sources.items():
|
|
186
210
|
target = skills_root / name / 'SKILL.md'
|
|
211
|
+
# A kept override belongs to the user, including their subsequent edits.
|
|
212
|
+
if str(target) in overrides and target.is_file():
|
|
213
|
+
state.get('skills', {}).pop(str(target), None)
|
|
214
|
+
continue
|
|
215
|
+
overrides.discard(str(target))
|
|
187
216
|
if target.is_symlink() or target.parent.is_symlink():
|
|
188
217
|
if target.is_file() and target.read_text() == content:
|
|
189
218
|
continue
|
|
190
219
|
raise ValueError(f'Existing skill symlink differs: {target}')
|
|
191
220
|
if target.exists() and target.read_text() != content:
|
|
192
221
|
if state.get('skills', {}).get(str(target)) != _hash(target.read_text()):
|
|
193
|
-
|
|
222
|
+
if resolve(target, 'skill') == 'keep':
|
|
223
|
+
overrides.add(str(target))
|
|
224
|
+
state.get('skills', {}).pop(str(target), None)
|
|
225
|
+
continue
|
|
194
226
|
writes[target] = content
|
|
227
|
+
if set(decisions) != examined:
|
|
228
|
+
raise ValueError('Conflicting files changed after your choice; run setup again.')
|
|
229
|
+
if conflicts and not collect_conflicts:
|
|
230
|
+
raise ValueError(conflict_message(conflicts))
|
|
231
|
+
state['skill_overrides'] = sorted(overrides)
|
|
195
232
|
return {'state_path': state_path, 'state': state, 'writes': writes,
|
|
196
|
-
'registrations': registrations}
|
|
233
|
+
'registrations': registrations, 'conflicts': conflicts, 'replacements': replacements}
|
|
197
234
|
|
|
198
235
|
|
|
199
|
-
def install_clients(clients: dict[str, str], env_file: Path, command: list[str]
|
|
236
|
+
def install_clients(clients: dict[str, str], env_file: Path, command: list[str], *,
|
|
237
|
+
resolutions: Resolutions | None = None) -> list[str]:
|
|
200
238
|
# Revalidate after human input: files may have changed since the preflight.
|
|
201
|
-
plan = prepare_clients(clients, env_file, command)
|
|
239
|
+
plan = prepare_clients(clients, env_file, command, resolutions=resolutions)
|
|
202
240
|
state, state_path = plan['state'], plan['state_path']
|
|
241
|
+
try:
|
|
242
|
+
backup_replacements(plan['replacements'], state_path.parent / 'backups')
|
|
243
|
+
except OSError as exc:
|
|
244
|
+
raise ValueError(f'Could not back up conflicting files in {state_path.parent / "backups"}. '
|
|
245
|
+
'Originals were not replaced. Check permissions and free space, then retry setup.') from exc
|
|
246
|
+
replacements = {item.path: item for item in plan['replacements']}
|
|
203
247
|
retry = ['uvx', 'mlspace-plugin@latest', 'setup', '--force']
|
|
204
248
|
for client, executable, *_ in plan['registrations']:
|
|
205
249
|
retry.extend(['--client-path', f'{client}={executable}'])
|
|
@@ -211,6 +255,8 @@ def install_clients(clients: dict[str, str], env_file: Path, command: list[str])
|
|
|
211
255
|
text = config.read_text() if config.exists() else '{}\n'
|
|
212
256
|
updated = set_jsonc_member(text, ['mcp', 'mlspace'], entry)
|
|
213
257
|
if updated != text:
|
|
258
|
+
if config in replacements:
|
|
259
|
+
replacements[config].verify_unchanged()
|
|
214
260
|
atomic_write(config, updated)
|
|
215
261
|
saved = _read_config(config).get('mcp', {}).get('mlspace')
|
|
216
262
|
if saved != entry:
|
|
@@ -236,11 +282,14 @@ def install_clients(clients: dict[str, str], env_file: Path, command: list[str])
|
|
|
236
282
|
try:
|
|
237
283
|
for target, content in plan['writes'].items():
|
|
238
284
|
if not target.exists() or target.read_text() != content:
|
|
285
|
+
if target in replacements:
|
|
286
|
+
replacements[target].verify_unchanged()
|
|
239
287
|
atomic_write(target, content, 0o644)
|
|
240
288
|
state.setdefault('skills', {})[str(target)] = _hash(content)
|
|
241
289
|
atomic_write(state_path, json.dumps(state, indent=2))
|
|
242
290
|
except (OSError, ValueError) as exc:
|
|
291
|
+
reason = str(exc) if type(exc) is ValueError else 'Check permissions in the skills directory.'
|
|
243
292
|
raise ValueError(f'MCP registered: {", ".join(installed)}. Skill installation incomplete; '
|
|
244
|
-
'
|
|
293
|
+
f'{reason} Rerun setup --force. '
|
|
245
294
|
'Verified credentials remain saved.') from exc
|
|
246
295
|
return installed
|
|
@@ -15,6 +15,7 @@ from urllib.parse import urlsplit
|
|
|
15
15
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
18
|
+
from . import USER_AGENT
|
|
18
19
|
from .client_setup import (
|
|
19
20
|
CLIENTS,
|
|
20
21
|
atomic_write,
|
|
@@ -33,6 +34,7 @@ from .config import (
|
|
|
33
34
|
read_dotenv_file,
|
|
34
35
|
user_config_file,
|
|
35
36
|
)
|
|
37
|
+
from .setup_conflicts import Resolutions, choose_resolutions
|
|
36
38
|
from .setup_ui import choose_workspaces, display, masked_prompt
|
|
37
39
|
from .setup_verification import verify_client
|
|
38
40
|
from .tls import safe_error as _safe_error
|
|
@@ -64,7 +66,9 @@ def _fetch_workspaces(key_id: str, key_secret: str, *, base_url: str,
|
|
|
64
66
|
from .client import MLSpaceClient
|
|
65
67
|
|
|
66
68
|
async def fetch() -> Any:
|
|
67
|
-
async with httpx.AsyncClient(
|
|
69
|
+
async with httpx.AsyncClient(
|
|
70
|
+
timeout=30, verify=tls_context(ca_file), headers={"User-Agent": USER_AGENT},
|
|
71
|
+
) as http:
|
|
68
72
|
tokens = TokenManager(http, base_url=base_url, client_id=key_id, client_secret=key_secret)
|
|
69
73
|
client = MLSpaceClient(http, tokens, base_url=base_url, api_key='', workspace_id='')
|
|
70
74
|
return await client.request('GET', '/public/v2/workspaces/v3/')
|
|
@@ -147,7 +151,10 @@ def _verify(values: dict[str, str], *, base_url: str) -> str | None:
|
|
|
147
151
|
workspaces=json.loads(values.get("WORKSPACES", "[]")),
|
|
148
152
|
namespace="",
|
|
149
153
|
)
|
|
150
|
-
async with httpx.AsyncClient(
|
|
154
|
+
async with httpx.AsyncClient(
|
|
155
|
+
timeout=30, verify=tls_context(values.get("CA_FILE", "")),
|
|
156
|
+
headers={"User-Agent": USER_AGENT},
|
|
157
|
+
) as http:
|
|
151
158
|
tokens = TokenManager(
|
|
152
159
|
http, base_url=base_url, client_id=values["CLIENT_ID"],
|
|
153
160
|
client_secret=values["CLIENT_SECRET"],
|
|
@@ -251,7 +258,7 @@ def run_init(path: Path | None = None, *, force: bool = False, verify: bool = Tr
|
|
|
251
258
|
except (ValueError, OSError, httpx.HTTPError) as exc:
|
|
252
259
|
# Only our own ValueErrors carry displayable text. Third-party errors may echo keys.
|
|
253
260
|
message = str(exc) if type(exc) is ValueError else _safe_error(exc)
|
|
254
|
-
print('Setup failed: ' + display(message), file=sys.stderr)
|
|
261
|
+
print('Setup failed: ' + '\n'.join(display(line) for line in message.splitlines()), file=sys.stderr)
|
|
255
262
|
return 1
|
|
256
263
|
|
|
257
264
|
|
|
@@ -284,8 +291,10 @@ def _run_init(path: Path | None, **options: Any) -> int:
|
|
|
284
291
|
selected_clients = {} if options['config_only'] or listing else _clients(
|
|
285
292
|
options['clients'], options['client_paths'], non_interactive)
|
|
286
293
|
command = server_command() if selected_clients and 'opencode' in selected_clients | managed_clients(selected_clients) else []
|
|
294
|
+
resolutions: Resolutions = {}
|
|
287
295
|
if selected_clients:
|
|
288
|
-
plan = prepare_clients(selected_clients, target, command)
|
|
296
|
+
plan = prepare_clients(selected_clients, target, command, collect_conflicts=True)
|
|
297
|
+
resolutions = choose_resolutions(plan.get('conflicts', []), non_interactive=non_interactive)
|
|
289
298
|
print('MCP and skills will be kept together for: ' +
|
|
290
299
|
', '.join(row[0] for row in plan['registrations']), file=sys.stderr)
|
|
291
300
|
if not listing:
|
|
@@ -385,11 +394,11 @@ def _run_init(path: Path | None, **options: Any) -> int:
|
|
|
385
394
|
if selected_clients:
|
|
386
395
|
print('[4/4] Connecting MCP and installing skills…', file=sys.stderr)
|
|
387
396
|
try:
|
|
388
|
-
install_clients(selected_clients, target, command)
|
|
397
|
+
install_clients(selected_clients, target, command, resolutions=resolutions)
|
|
389
398
|
for client, executable, *_ in plan['registrations']:
|
|
390
399
|
print(f' {client}: installed; checking skills and MCP process…', file=sys.stderr)
|
|
391
400
|
verify_client(client, executable, target, command)
|
|
392
|
-
print(f' {client}: skills
|
|
401
|
+
print(f' {client}: packaged skills checked (kept user overrides excluded); MCP process verified.', file=sys.stderr)
|
|
393
402
|
except (OSError, ValueError) as exc:
|
|
394
403
|
reason = str(exc) if type(exc) is ValueError else _safe_error(exc)
|
|
395
404
|
raise ValueError('Credentials saved; client setup is incomplete. ' + reason) from exc
|
|
@@ -14,6 +14,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
14
14
|
from mcp.server.transport_security import TransportSecuritySettings
|
|
15
15
|
from mcp.types import ToolAnnotations
|
|
16
16
|
|
|
17
|
+
from . import USER_AGENT
|
|
17
18
|
from .auth import TokenManager
|
|
18
19
|
from .config import Settings
|
|
19
20
|
from .overview import register_overviews
|
|
@@ -149,7 +150,10 @@ def build_server(settings: Settings) -> FastMCP:
|
|
|
149
150
|
|
|
150
151
|
@asynccontextmanager
|
|
151
152
|
async def lifespan(_server: FastMCP):
|
|
152
|
-
async with httpx.AsyncClient(
|
|
153
|
+
async with httpx.AsyncClient(
|
|
154
|
+
timeout=settings.timeout, verify=tls_context(settings.ca_file),
|
|
155
|
+
headers={"User-Agent": USER_AGENT},
|
|
156
|
+
) as http:
|
|
153
157
|
tokens = TokenManager(
|
|
154
158
|
http,
|
|
155
159
|
settings.base_url,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Explicit choices and recoverable backups for existing OpenCode files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .setup_ui import display
|
|
13
|
+
|
|
14
|
+
Resolutions = dict[str, tuple[str, str]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fingerprint(path: Path) -> str:
|
|
18
|
+
"""Include supporting skill files; never follow symlinks while inspecting them."""
|
|
19
|
+
rows = []
|
|
20
|
+
paths = [path, *sorted(path.rglob('*'))] if path.is_dir() and not path.is_symlink() else [path]
|
|
21
|
+
for item in paths:
|
|
22
|
+
name = str(item.relative_to(path))
|
|
23
|
+
if item.is_symlink():
|
|
24
|
+
value = 'link:' + str(item.readlink())
|
|
25
|
+
elif item.is_file():
|
|
26
|
+
value = hashlib.sha256(item.read_bytes()).hexdigest()
|
|
27
|
+
elif item.is_dir():
|
|
28
|
+
value = 'directory'
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(f'Unsupported or missing setup file: {item}')
|
|
31
|
+
rows.append((name, value))
|
|
32
|
+
return hashlib.sha256(json.dumps(rows).encode()).hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Conflict:
|
|
37
|
+
path: Path
|
|
38
|
+
kind: str
|
|
39
|
+
fingerprint: str
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def source(self) -> Path:
|
|
43
|
+
return self.path.parent if self.kind == 'skill' else self.path
|
|
44
|
+
|
|
45
|
+
def verify_unchanged(self) -> None:
|
|
46
|
+
if fingerprint(self.source) != self.fingerprint:
|
|
47
|
+
raise ValueError(f'File changed after your choice; run setup again: {self.path}')
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def inspect(cls, path: Path, kind: str) -> Conflict:
|
|
51
|
+
source = path.parent if kind == 'skill' else path
|
|
52
|
+
return cls(path, kind, fingerprint(source))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def conflict_message(conflicts: list[Conflict]) -> str:
|
|
56
|
+
lines = ['Existing MLSpace files differ; nothing was overwritten:']
|
|
57
|
+
lines.extend(f' {item.kind}: {item.path}' for item in conflicts)
|
|
58
|
+
lines.append('Run setup in your terminal to keep custom skills or back up and replace conflicting files. '
|
|
59
|
+
'--force does not approve replacing them.')
|
|
60
|
+
return '\n'.join(lines)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def choose_resolutions(conflicts: list[Conflict], *, non_interactive: bool) -> Resolutions:
|
|
64
|
+
if not conflicts:
|
|
65
|
+
return {}
|
|
66
|
+
if non_interactive:
|
|
67
|
+
raise ValueError(conflict_message(conflicts))
|
|
68
|
+
for line in conflict_message(conflicts).splitlines():
|
|
69
|
+
print(display(line), file=sys.stderr)
|
|
70
|
+
print('Replacement saves a backup outside the skills directory. '
|
|
71
|
+
'Other connections and additional user skills are preserved.', file=sys.stderr)
|
|
72
|
+
choices = {}
|
|
73
|
+
for item in conflicts:
|
|
74
|
+
print(display(str(item.path)), file=sys.stderr)
|
|
75
|
+
prompt = ('[k] Keep my skill (exclude from plugin updates), [r] Back up and replace, '
|
|
76
|
+
'[Enter] Cancel: ' if item.kind == 'skill' else
|
|
77
|
+
'[r] Back up config and replace only mcp.mlspace, [Enter] Cancel: ')
|
|
78
|
+
while True:
|
|
79
|
+
choice = input(prompt).strip().lower()
|
|
80
|
+
if not choice:
|
|
81
|
+
raise ValueError('Setup cancelled; no conflicting files were changed.')
|
|
82
|
+
if choice == 'r' or (choice == 'k' and item.kind == 'skill'):
|
|
83
|
+
choices[str(item.path)] = ('keep' if choice == 'k' else 'replace', item.fingerprint)
|
|
84
|
+
break
|
|
85
|
+
print('Choose one of the displayed options or press Enter to cancel.', file=sys.stderr)
|
|
86
|
+
return choices
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def backup_replacements(conflicts: list[Conflict], root: Path) -> Path | None:
|
|
90
|
+
if not conflicts:
|
|
91
|
+
return None
|
|
92
|
+
root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
93
|
+
backup = Path(tempfile.mkdtemp(prefix='setup-', dir=root))
|
|
94
|
+
print(f'Backup directory: {display(str(backup))}', file=sys.stderr)
|
|
95
|
+
for item in conflicts:
|
|
96
|
+
item.verify_unchanged()
|
|
97
|
+
destination = backup / 'skills' / item.path.parent.name if item.kind == 'skill' else backup / item.path.name
|
|
98
|
+
if item.kind == 'skill':
|
|
99
|
+
shutil.copytree(item.source, destination, symlinks=True)
|
|
100
|
+
else:
|
|
101
|
+
shutil.copy2(item.source, destination)
|
|
102
|
+
destination.chmod(0o600)
|
|
103
|
+
if fingerprint(destination) != item.fingerprint or fingerprint(item.source) != item.fingerprint:
|
|
104
|
+
raise ValueError(f'File changed during backup; originals were not replaced: {item.path}')
|
|
105
|
+
return backup
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
|
+
import sys
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
@@ -13,10 +14,11 @@ from mcp import ClientSession, StdioServerParameters
|
|
|
13
14
|
from mcp.client.stdio import stdio_client
|
|
14
15
|
|
|
15
16
|
from . import __version__
|
|
16
|
-
from .client_setup import _config, _read_config, _skill_sources
|
|
17
|
+
from .client_setup import _config, _read_config, _skill_sources, install_state_file
|
|
17
18
|
from .config import Settings, read_dotenv_file
|
|
18
19
|
from .native_plugins import inspect_native
|
|
19
20
|
from .server import build_server
|
|
21
|
+
from .setup_ui import display
|
|
20
22
|
from .workspace_context import environment_id, normalized_endpoint, selected_workspaces
|
|
21
23
|
|
|
22
24
|
|
|
@@ -73,11 +75,15 @@ def verify_runtime(command: list[str], credentials: Path, *, direct: bool = Fals
|
|
|
73
75
|
|
|
74
76
|
def verify_client(client: str, executable: str, credentials: Path, command: list[str]) -> None:
|
|
75
77
|
sources = _skill_sources()
|
|
78
|
+
overrides: set[str] = set()
|
|
76
79
|
if client == 'opencode':
|
|
77
80
|
config = _read_config(_config(client)).get('mcp', {}).get('mlspace', {})
|
|
78
81
|
if config.get('command') != command or config.get('enabled') is not True:
|
|
79
82
|
raise ValueError('opencode: installed MCP configuration differs; retry setup.')
|
|
80
83
|
root = Path(os.environ.get('XDG_CONFIG_HOME', str(Path.home() / '.config'))) / 'opencode/skills'
|
|
84
|
+
state_path = install_state_file()
|
|
85
|
+
if state_path.exists():
|
|
86
|
+
overrides = set(json.loads(state_path.read_text()).get('skill_overrides', []))
|
|
81
87
|
cwd = None
|
|
82
88
|
else:
|
|
83
89
|
status = inspect_native(client, executable)
|
|
@@ -89,7 +95,12 @@ def verify_client(client: str, executable: str, credentials: Path, command: list
|
|
|
89
95
|
command = ['uvx', '--from', f'mlspace-plugin=={__version__}', 'mlspace-plugin', '--transport', 'stdio']
|
|
90
96
|
if config != {'type': 'stdio', 'command': command[0], 'args': command[1:]}:
|
|
91
97
|
raise ValueError(f'{client}: installed runtime command differs from this release.')
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
for name, content in sources.items():
|
|
99
|
+
path = root / name / 'SKILL.md'
|
|
100
|
+
if not path.is_file():
|
|
101
|
+
raise ValueError(f'{client}: installed skills missing: {path}; retry setup.')
|
|
102
|
+
if client == 'opencode' and str(path) in overrides:
|
|
103
|
+
print(f' User skill preserved (not verified against release): {display(str(path))}', file=sys.stderr)
|
|
104
|
+
elif path.read_text() != content:
|
|
105
|
+
raise ValueError(f'{client}: installed skills differ from this release: {path}; retry setup.')
|
|
95
106
|
verify_runtime(command, credentials, direct=client == 'opencode', cwd=cwd)
|
|
@@ -239,7 +239,7 @@ def test_modified_previously_connected_client_blocks_shared_update(tmp_path, mon
|
|
|
239
239
|
client_setup.install_clients({'opencode': '/usr/bin/true'}, tmp_path / '.env', ['runtime-v1'])
|
|
240
240
|
config = tmp_path / '.config/opencode/opencode.json'
|
|
241
241
|
config.write_text(config.read_text().replace('runtime-v1', 'user-edited'))
|
|
242
|
-
with pytest.raises(ValueError, match='was
|
|
242
|
+
with pytest.raises(ValueError, match='nothing was overwritten'):
|
|
243
243
|
client_setup.prepare_clients({'codex': '/usr/bin/true'}, tmp_path / '.env', ['runtime-v2'])
|
|
244
244
|
|
|
245
245
|
|
|
@@ -10,6 +10,62 @@ from mlspace_mcp.init_cli import run_init
|
|
|
10
10
|
BASE = 'https://setup.example'
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def conflicting_opencode(tmp_path, monkeypatch):
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from mlspace_mcp import init_cli
|
|
17
|
+
monkeypatch.setattr(sys.stdin, 'isatty', lambda: True)
|
|
18
|
+
monkeypatch.setattr(init_cli, 'detect_clients', lambda: {'opencode': '/usr/bin/true'})
|
|
19
|
+
monkeypatch.setattr(init_cli, 'server_command', lambda: [sys.executable, '-m', 'mlspace_mcp', '--transport', 'stdio'])
|
|
20
|
+
root = tmp_path / '.config/opencode'
|
|
21
|
+
skill = root / 'skills/mlspace-list-running-jobs/SKILL.md'
|
|
22
|
+
skill.parent.mkdir(parents=True)
|
|
23
|
+
skill.write_text('custom workflow')
|
|
24
|
+
config = root / 'opencode.jsonc'
|
|
25
|
+
config.write_text('{"mcp":{"mlspace":{"command":["old-wrapper"]}}}')
|
|
26
|
+
return skill, config
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@respx.mock
|
|
30
|
+
def test_interactive_setup_resolves_wrapper_and_keeps_custom_skill(tmp_path, monkeypatch, capsys):
|
|
31
|
+
routes()
|
|
32
|
+
skill, config = conflicting_opencode(tmp_path, monkeypatch)
|
|
33
|
+
answers = iter(['r', 'k'])
|
|
34
|
+
monkeypatch.setattr('builtins.input', lambda *a: next(answers))
|
|
35
|
+
target = tmp_path / 'new.env'
|
|
36
|
+
assert run_init(target, base_url=BASE, from_env=True, clients=['opencode'], workspace_ids=['ws2']) == 0
|
|
37
|
+
assert skill.read_text() == 'custom workflow'
|
|
38
|
+
assert 'old-wrapper' not in config.read_text()
|
|
39
|
+
assert read_dotenv_file(target)['MLSPACE_WORKSPACE_ID'] == 'ws2'
|
|
40
|
+
output = capsys.readouterr().err
|
|
41
|
+
assert str(config) in output and str(skill) in output
|
|
42
|
+
assert output.index(str(skill)) < output.index('[1/4]')
|
|
43
|
+
assert 'not verified against release' in output
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_cancel_conflicts_preserves_configuration_and_credentials(tmp_path, monkeypatch, capsys):
|
|
47
|
+
skill, config = conflicting_opencode(tmp_path, monkeypatch)
|
|
48
|
+
before = config.read_bytes()
|
|
49
|
+
monkeypatch.setattr('builtins.input', lambda *a: '')
|
|
50
|
+
target = tmp_path / 'new.env'
|
|
51
|
+
assert run_init(target, base_url=BASE, from_env=True, clients=['opencode'], workspace_ids=['ws2']) == 1
|
|
52
|
+
assert not target.exists()
|
|
53
|
+
assert config.read_bytes() == before and skill.read_text() == 'custom workflow'
|
|
54
|
+
assert 'cancelled' in capsys.readouterr().err.lower()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_noninteractive_conflicts_list_all_paths_without_prompt_even_with_force(tmp_path, monkeypatch, capsys):
|
|
58
|
+
skill, config = conflicting_opencode(tmp_path, monkeypatch)
|
|
59
|
+
monkeypatch.setattr('builtins.input', lambda *a: pytest.fail('unexpected prompt'))
|
|
60
|
+
target = tmp_path / 'existing.env'
|
|
61
|
+
target.write_text('MLSPACE_CLIENT_SECRET=old-secret\n')
|
|
62
|
+
assert run_init(target, base_url=BASE, from_env=True, clients=['opencode'], workspace_ids=['ws2'],
|
|
63
|
+
non_interactive=True, force=True) == 1
|
|
64
|
+
output = capsys.readouterr().err
|
|
65
|
+
assert str(skill) in output and str(config) in output
|
|
66
|
+
assert target.read_text() == 'MLSPACE_CLIENT_SECRET=old-secret\n'
|
|
67
|
+
|
|
68
|
+
|
|
13
69
|
@pytest.fixture(autouse=True)
|
|
14
70
|
def isolate(monkeypatch, tmp_path):
|
|
15
71
|
import os
|
|
@@ -163,7 +219,7 @@ def test_client_install_failure_keeps_verified_credentials_for_retry(tmp_path, m
|
|
|
163
219
|
target.write_text(original)
|
|
164
220
|
monkeypatch.setattr('mlspace_mcp.init_cli._clients', lambda *a: {'opencode': '/bin/true'})
|
|
165
221
|
monkeypatch.setattr('mlspace_mcp.init_cli.server_command', lambda: ['/bin/echo'])
|
|
166
|
-
monkeypatch.setattr('mlspace_mcp.init_cli.prepare_clients', lambda *a: {'registrations': [('opencode',)]})
|
|
222
|
+
monkeypatch.setattr('mlspace_mcp.init_cli.prepare_clients', lambda *a, **kw: {'registrations': [('opencode',)]})
|
|
167
223
|
def fail(*a, **kw):
|
|
168
224
|
raise ValueError('Client installation failed')
|
|
169
225
|
monkeypatch.setattr('mlspace_mcp.init_cli.install_clients', fail)
|