opalacoder 0.2.0__tar.gz → 0.2.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.
- opalacoder-0.2.2/AppIcons/icon.png +0 -0
- opalacoder-0.2.2/AppIcons/opalaicon.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/PKG-INFO +1 -1
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/07-configuracao.md +217 -1
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/src/App.jsx +66 -2
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/src/index.css +5 -0
- opalacoder-0.2.2/icon.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/agent_stdin.py +16 -2
- opalacoder-0.2.2/opalacoder/assetstore/modelconfigs/gpt_oss__latest.metadata +4 -0
- opalacoder-0.2.2/opalacoder/assetstore/modelconfigs/gpt_oss__latest.zip +0 -0
- opalacoder-0.2.2/opalacoder/assetstore/skills/skill_html_css_js.metadata +4 -0
- opalacoder-0.2.2/opalacoder/assetstore/skills/skill_html_css_js.zip +0 -0
- opalacoder-0.2.2/opalacoder/assetstore/skills/skill_implement_feature.metadata +4 -0
- opalacoder-0.2.2/opalacoder/assetstore/skills/skill_implement_feature.zip +0 -0
- opalacoder-0.2.2/opalacoder/assetstore.py +179 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/cli_commands.py +78 -0
- opalacoder-0.2.2/opalacoder/gui/assets/index-CD7_Sjss.css +32 -0
- opalacoder-0.2.2/opalacoder/gui/assets/index-D2mJbeF2.js +159 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/gui/index.html +2 -2
- opalacoder-0.2.2/opalacoder/icon.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/ide_server.py +58 -1
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/memgpt_runtime.py +2 -1
- {opalacoder-0.2.0 → opalacoder-0.2.2}/pyproject.toml +1 -1
- opalacoder-0.2.0/opalacoder/gui/assets/index-BqTKFzta.js +0 -159
- opalacoder-0.2.0/opalacoder/gui/assets/index-_ON06SYj.css +0 -32
- opalacoder-0.2.0/opalacoder/icon.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/.claude/rules/RULES.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/.claude/settings.json +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/.github/workflows/publish.yml +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/.gitignore +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AGENTS.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/100.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/102.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/1024.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/108.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/114.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/120.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/128.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/144.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/152.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/16.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/167.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/172.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/180.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/196.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/20.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/216.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/234.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/256.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/258.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/29.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/32.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/40.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/48.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/50.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/512.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/55.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/57.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/58.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/60.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/64.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/66.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/72.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/76.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/80.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/87.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/88.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/92.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/android/mipmap-hdpi/ic_launcher.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/android/mipmap-mdpi/ic_launcher.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/android/mipmap-xhdpi/ic_launcher.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/appstore.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/AppIcons/playstore.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/CLAUDE.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/GEMINI.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/README.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/agents.yaml +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/config.yaml +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2/debug}/debug_litellm_callbacks.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/debug/scratch_tail.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2/debug}/test_thought_stream.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/guide/analysis_results.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/guide/figs/logotipo.png +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/guide/paper_memplan.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/guide/skills-plugin-system.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/01-arquitetura.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/02-memgpt-orquestrador.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/03-skill-implement-feature.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/04-memoria.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/05-vcs-sombra.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/06-skills-e-plugins.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/08-ide.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/09-ide-projectcreationinterface.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/README.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/docs/specs/backlog_register.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/index.html +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/package-lock.json +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/package.json +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/src/main.jsx +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/gui_src/vite.config.js +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/main.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/__init__.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/agents.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/api_keys.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/archival.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/cli.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/code_index.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/config.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/embeddings.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/gui/assets/vendor-monaco-BkbU5OES.js +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/gui/assets/vendor-react-B2vUwPEE.js +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/gui/assets/vendor-xterm-3VOAfa_q.js +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/i18n.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/orchestrator.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/planner.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/plugins/__init__.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/plugins/html_css_js_tools.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/project.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/session.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/skills.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/structured.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/terminal.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/terminal_manager.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/tools.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/vcs.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/vector_index.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/workflow_orchestrator.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/opalacoder/workflow_tools.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/pytest.ini +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/requirements.txt +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/chat-orchestrator/SKILL.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/command-line/SKILL.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/command-line/scripts/command_executor.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/skills_store/html-css-js/SKILL.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/skills_store/html-css-js/scripts/check_contracts.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/skills_store/implement-feature/SKILL.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/skills_store/implement-feature/scripts/run_workflow.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/view-editor/SKILL.md +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/skills/view-editor/scripts/run_view_editor.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_agent_config.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_agent_stdin.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_code_index_integration.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_command_line_skill.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_complexity_evaluator.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_context_guard.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_file_delete.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_i18n_coverage.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_implement_feature_script.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_memgpt_runtime.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_model_commands.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_planner_oracle_live.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_planner_output.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_project_store.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_refinement_loop.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_search_bugs.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_skills_directory_loader.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_unhashable_dict_bug.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_verification_strategies.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_view_editor_skill.py +0 -0
- {opalacoder-0.2.0 → opalacoder-0.2.2}/tests/test_workflow_pipeline.py +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opalacoder
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Autonomous coding agent with interactive planning and modular execution
|
|
5
5
|
Project-URL: Homepage, https://github.com/gilzamir/OpalaCoder
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/gilzamir/OpalaCoder/issues
|
|
@@ -141,7 +141,223 @@ De `build_parser` ([cli.py:419](../../opalacoder/cli.py#L419)):
|
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
144
|
-
## 5.
|
|
144
|
+
## 5. AssetStore — Repositório de Assets Reutilizáveis
|
|
145
|
+
|
|
146
|
+
> **Estado: IMPLEMENTADO**
|
|
147
|
+
|
|
148
|
+
### Conceito
|
|
149
|
+
|
|
150
|
+
A AssetStore é um **repositório local de assets reutilizáveis** empacotados com o OpalaCoder. Ela serve como banco de dados de casos bem-sucedidos de configuração para modelos locais e como biblioteca de skills extras que podem ser instaladas sob demanda em qualquer projeto.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
opalacoder/assetstore/ ← instalado junto com o pacote pip
|
|
154
|
+
skills/
|
|
155
|
+
<ID>.zip ← árvore completa da skill
|
|
156
|
+
<ID>.metadata ← metadados YAML
|
|
157
|
+
modelconfigs/
|
|
158
|
+
<ID>.zip ← único arquivo .yaml de configuração
|
|
159
|
+
<ID>.metadata ← metadados YAML
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
O caminho base é sempre `Path(__file__).parent / "assetstore"`, portanto funciona corretamente quando instalado via `pip install opalacoder` em qualquer máquina.
|
|
163
|
+
|
|
164
|
+
### Formato dos metadados (`.metadata`)
|
|
165
|
+
|
|
166
|
+
Arquivo YAML com os campos:
|
|
167
|
+
|
|
168
|
+
| Campo | Obrigatório | Descrição |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `id` | ✓ | Identificador único do asset (ex: `skill_implement_feature`) |
|
|
171
|
+
| `type` | ✓ | `skill` ou `modelconfig` |
|
|
172
|
+
| `desc` | ✓ | Descrição legível usada como critério de busca |
|
|
173
|
+
| `name` | skill | Nome da skill (igual ao diretório dentro do zip) |
|
|
174
|
+
| `model` | modelconfig | Identificador do modelo (ex: `ollama/gpt-oss:latest`) |
|
|
175
|
+
|
|
176
|
+
**Exemplo — skill:**
|
|
177
|
+
```yaml
|
|
178
|
+
id: skill_implement_feature
|
|
179
|
+
type: skill
|
|
180
|
+
name: implement-feature
|
|
181
|
+
desc: Plan-Execute-Verify loop for implementing features and fixing bugs in project files
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Exemplo — modelconfig:**
|
|
185
|
+
```yaml
|
|
186
|
+
id: model_ollama_gpt_oss__latest
|
|
187
|
+
type: modelconfig
|
|
188
|
+
desc: gpt-oss:latest via Ollama — think=false, tool calling stable
|
|
189
|
+
model: ollama/gpt-oss:latest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Destino de instalação
|
|
193
|
+
|
|
194
|
+
| Tipo | Destino no projeto |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `skill` | `<projeto>/.opalacoder/skills/<name>/` |
|
|
197
|
+
| `modelconfig` | `<projeto>/.opalacoder/modelsconfig/<provider>/<modelo>.yaml` |
|
|
198
|
+
|
|
199
|
+
Para modelconfigs, o provider é normalizado: `ollama_chat/` e `ollama/` → diretório `ollama/`. Os dois pontos no nome do modelo viram `__`.
|
|
200
|
+
|
|
201
|
+
### Comandos REPL
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
/list_assets [tipo] — lista todos os assets (ou só do tipo especificado)
|
|
205
|
+
/load_asset <tipo> <desc|id|*> — instala asset(s) no projeto ativo
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`<desc>` pode ser o `id` exato ou o valor do campo `desc`. Usar `*` instala todos os assets do tipo.
|
|
209
|
+
|
|
210
|
+
**Exemplos:**
|
|
211
|
+
```
|
|
212
|
+
/list_assets
|
|
213
|
+
/list_assets modelconfig
|
|
214
|
+
/load_asset skill skill_implement_feature
|
|
215
|
+
/load_asset modelconfig model_ollama_gpt_oss__latest
|
|
216
|
+
/load_asset skill *
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Após instalar uma skill, ativá-la no projeto com `/addskill <name>`.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 5.1. Banco de Configurações Refinadas de Modelos
|
|
224
|
+
|
|
225
|
+
> **Estado: IMPLEMENTADO**
|
|
226
|
+
|
|
227
|
+
### Conceito
|
|
228
|
+
|
|
229
|
+
O diretório `.opalacoder/modelsconfig/` dentro de cada projeto funciona como um
|
|
230
|
+
**banco de dados de casos bem-sucedidos de configuração para modelos locais**. Cada
|
|
231
|
+
arquivo YAML registra os parâmetros que produziram comportamento correto e estável
|
|
232
|
+
para um modelo específico naquele projeto — resultado de testes empíricos, não de
|
|
233
|
+
valores genéricos.
|
|
234
|
+
|
|
235
|
+
A motivação é que modelos locais (Ollama, LM Studio, etc.) exigem combinações
|
|
236
|
+
precisas de parâmetros para funcionar bem: `think` e `stream` podem quebrar
|
|
237
|
+
tool calling em certos modelos; `num_ctx` muito baixo degrada o raciocínio;
|
|
238
|
+
`temperature` alta pode destabilizar modelos de instrução. Esses parâmetros
|
|
239
|
+
**não são portáveis** — o que funciona para `deepseek-r1:14b` não funciona para
|
|
240
|
+
`ministral-3:14b`. O banco de configurações resolve isso por projeto.
|
|
241
|
+
|
|
242
|
+
### Estrutura de diretórios
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
<projeto>/
|
|
246
|
+
└── .opalacoder/
|
|
247
|
+
└── modelsconfig/
|
|
248
|
+
└── <provider>/
|
|
249
|
+
└── <nome_do_modelo>.yaml
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- **`<provider>`**: prefixo do modelo, com normalização — `ollama_chat/` e `ollama/`
|
|
253
|
+
mapeiam ambos para o diretório `ollama/`.
|
|
254
|
+
- **`<nome_do_modelo>.yaml`**: nome do modelo com `:` substituído por `__`
|
|
255
|
+
(hífens mantidos).
|
|
256
|
+
|
|
257
|
+
**Exemplos de mapeamento:**
|
|
258
|
+
|
|
259
|
+
| Modelo digitado | Diretório | Arquivo |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `ollama/deepseek-r1:14b` | `ollama/` | `deepseek-r1__14b.yaml` |
|
|
262
|
+
| `ollama_chat/deepseek-r1:14b` | `ollama/` | `deepseek-r1__14b.yaml` |
|
|
263
|
+
| `ollama/ministral-3:14b` | `ollama/` | `ministral-3__14b.yaml` |
|
|
264
|
+
| `ollama/qwen3:14b` | `ollama/` | `qwen3__14b.yaml` |
|
|
265
|
+
|
|
266
|
+
### Formato do arquivo YAML
|
|
267
|
+
|
|
268
|
+
O arquivo pode conter qualquer chave de `model_params` aceita pelo sistema, mais
|
|
269
|
+
uma chave especial `provider`:
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
# .opalacoder/modelsconfig/ollama/deepseek-r1__14b.yaml
|
|
273
|
+
# Configuração refinada para DeepSeek-R1 14B via Ollama
|
|
274
|
+
# Obtida empiricamente — funciona com tool calling e thinking em tempo real
|
|
275
|
+
|
|
276
|
+
provider: ollama_chat # opcional: substitui o prefixo do modelo na sessão
|
|
277
|
+
# aqui muda de ollama/ para ollama_chat/ para habilitar
|
|
278
|
+
# o endpoint nativo do Ollama com thinking por chunk
|
|
279
|
+
|
|
280
|
+
# LiteLLM / model_kwargs
|
|
281
|
+
think: true
|
|
282
|
+
stream: true
|
|
283
|
+
temperature: 0.6
|
|
284
|
+
num_ctx: 32768
|
|
285
|
+
max_tokens: 8192
|
|
286
|
+
|
|
287
|
+
# Parâmetros do agente
|
|
288
|
+
max_heartbeats: 15
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
# .opalacoder/modelsconfig/ollama/ministral-3__14b.yaml
|
|
293
|
+
# Ministral 3 14B — sem thinking (não suportado), tool calling estável
|
|
294
|
+
|
|
295
|
+
think: false
|
|
296
|
+
stream: false
|
|
297
|
+
temperature: 0.7
|
|
298
|
+
num_ctx: 16384
|
|
299
|
+
max_tokens: 8128
|
|
300
|
+
max_heartbeats: 20
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Chave `provider`** (opcional): quando presente, substitui o prefixo do modelo
|
|
304
|
+
na interface. Útil para forçar `ollama_chat/` em modelos com suporte a thinking
|
|
305
|
+
nativo, sem o usuário precisar digitar o prefixo correto.
|
|
306
|
+
|
|
307
|
+
### Como usar
|
|
308
|
+
|
|
309
|
+
Na janela de **criação** ou **configuração** de projeto, após definir o modelo,
|
|
310
|
+
clique em **Load Refined Config**:
|
|
311
|
+
|
|
312
|
+
- Se existir um arquivo para o modelo: carrega e substitui completamente o
|
|
313
|
+
`model_params` do projeto. Se o YAML tiver `provider:`, atualiza o campo
|
|
314
|
+
modelo com o novo prefixo.
|
|
315
|
+
- Se não existir: exibe `--- ainda não temos parâmetros refinados para este modelo`.
|
|
316
|
+
|
|
317
|
+
O backend resolve o arquivo via `GET/POST /api/opalacoder/model-config`
|
|
318
|
+
([ide_server.py](../../opalacoder/ide_server.py)).
|
|
319
|
+
|
|
320
|
+
### Critério para adicionar uma entrada
|
|
321
|
+
|
|
322
|
+
Uma entrada deve ser adicionada ao banco quando:
|
|
323
|
+
|
|
324
|
+
1. O modelo foi testado com a configuração e **tool calling funciona** sem erros.
|
|
325
|
+
2. O comportamento de **thinking/reflection** (se aplicável) está correto.
|
|
326
|
+
3. Os valores de **contexto e tokens** são adequados ao hardware disponível.
|
|
327
|
+
|
|
328
|
+
O arquivo é mantido manualmente pelo usuário/equipe e versionado junto com o
|
|
329
|
+
projeto. Não é gerado automaticamente.
|
|
330
|
+
|
|
331
|
+
### Exemplo completo — `gpt-oss:latest` (Ollama)
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
# .opalacoder/modelsconfig/ollama/gpt-oss__latest.yaml
|
|
335
|
+
#
|
|
336
|
+
# Configuração refinada para gpt-oss:latest via Ollama
|
|
337
|
+
# Testado em: 2026-06-02 | Hardware: RTX 4090 24GB
|
|
338
|
+
# Status: tool calling OK, thinking OK, stream OK
|
|
339
|
+
#
|
|
340
|
+
provider: ollama_chat # endpoint nativo: thinking por chunk em tempo real
|
|
341
|
+
|
|
342
|
+
# LiteLLM / model_kwargs
|
|
343
|
+
think: false # desativar thinking evita bug de tool_calls vazio (ollama #15288)
|
|
344
|
+
stream: false # stream false: mais estável com tool calling
|
|
345
|
+
temperature: 0.1 # baixo para respostas determinísticas em tarefas de código
|
|
346
|
+
num_ctx: 32768 # contexto amplo para projetos grandes
|
|
347
|
+
max_tokens: 8192
|
|
348
|
+
|
|
349
|
+
# Parâmetros do agente MemGPT (chat-orquestrador)
|
|
350
|
+
max_heartbeats: 20
|
|
351
|
+
max_context_tokens: 32768
|
|
352
|
+
|
|
353
|
+
# Parâmetros do agente LLMAgentBlock (workers)
|
|
354
|
+
max_tool_calls: 10
|
|
355
|
+
debug: false
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 6. Comandos do REPL
|
|
145
361
|
|
|
146
362
|
Registrados em [cli_commands.py](../../opalacoder/cli_commands.py):
|
|
147
363
|
|
|
@@ -75,6 +75,7 @@ export default function App() {
|
|
|
75
75
|
const [thinkingLogs, setThinkingLogs] = useState([]);
|
|
76
76
|
const [showAdvancedParams, setShowAdvancedParams] = useState(false);
|
|
77
77
|
const [newProjError, setNewProjError] = useState('');
|
|
78
|
+
const [modelConfigMsg, setModelConfigMsg] = useState('');
|
|
78
79
|
|
|
79
80
|
// Directory picker state: { target: 'new'|'edit', current: '/path', dirs: [{name,path}] } | null
|
|
80
81
|
const [dirPicker, setDirPicker] = useState(null);
|
|
@@ -182,6 +183,7 @@ export default function App() {
|
|
|
182
183
|
const [newProjDesc, setNewProjDesc] = useState('');
|
|
183
184
|
const [newProjModel, setNewProjModel] = useState('gemini/gemini-2.5-flash');
|
|
184
185
|
const [newProjMode, setNewProjMode] = useState('auto');
|
|
186
|
+
const [newProjModelParams, setNewProjModelParams] = useState({});
|
|
185
187
|
const [newProjApiKey, setNewProjApiKey] = useState('');
|
|
186
188
|
const [newProjApiBase, setNewProjApiBase] = useState('http://localhost:11434/v1');
|
|
187
189
|
|
|
@@ -980,7 +982,8 @@ export default function App() {
|
|
|
980
982
|
model: newProjModel,
|
|
981
983
|
mode: newProjMode,
|
|
982
984
|
api_key: newProjApiKey,
|
|
983
|
-
api_base: newProjApiBase
|
|
985
|
+
api_base: newProjApiBase,
|
|
986
|
+
model_params: Object.keys(newProjModelParams).length ? newProjModelParams : undefined
|
|
984
987
|
})
|
|
985
988
|
});
|
|
986
989
|
if (res.ok) {
|
|
@@ -1035,6 +1038,7 @@ export default function App() {
|
|
|
1035
1038
|
if (found) fresh = found;
|
|
1036
1039
|
}
|
|
1037
1040
|
} catch (_) { }
|
|
1041
|
+
setModelConfigMsg('');
|
|
1038
1042
|
setEditingProject({
|
|
1039
1043
|
name: fresh.name,
|
|
1040
1044
|
project_name: fresh.project_name || fresh.name,
|
|
@@ -1087,6 +1091,30 @@ export default function App() {
|
|
|
1087
1091
|
}
|
|
1088
1092
|
};
|
|
1089
1093
|
|
|
1094
|
+
const loadModelConfig = async (projectPath, model, applyFn) => {
|
|
1095
|
+
setModelConfigMsg('');
|
|
1096
|
+
if (!projectPath || !model) {
|
|
1097
|
+
setModelConfigMsg('⚠️ Defina o caminho do projeto e o modelo antes de carregar.');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
const res = await fetch('/api/opalacoder/model-config', {
|
|
1102
|
+
method: 'POST',
|
|
1103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1104
|
+
body: JSON.stringify({ projectPath, model }),
|
|
1105
|
+
});
|
|
1106
|
+
const data = await res.json();
|
|
1107
|
+
if (!data.found) {
|
|
1108
|
+
setModelConfigMsg(data.message);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
applyFn(data);
|
|
1112
|
+
setModelConfigMsg('✅ Configuração refinada carregada.');
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
setModelConfigMsg(`⚠️ Erro ao carregar: ${e.message}`);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1090
1118
|
const openDirPicker = async (target, startPath) => {
|
|
1091
1119
|
const path = startPath || '~';
|
|
1092
1120
|
try {
|
|
@@ -1194,6 +1222,7 @@ export default function App() {
|
|
|
1194
1222
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `🔴 Falha: ${err.message}` }]);
|
|
1195
1223
|
} finally {
|
|
1196
1224
|
setIsAgentRunning(false);
|
|
1225
|
+
fetchFiles();
|
|
1197
1226
|
}
|
|
1198
1227
|
return;
|
|
1199
1228
|
}
|
|
@@ -1474,7 +1503,7 @@ export default function App() {
|
|
|
1474
1503
|
<div className="vscode-sidebar-header">
|
|
1475
1504
|
<span className="vscode-sidebar-title">EXPLORER: PROJECTS</span>
|
|
1476
1505
|
<button
|
|
1477
|
-
onClick={() => setShowNewProjectModal(true)}
|
|
1506
|
+
onClick={() => { setShowNewProjectModal(true); setModelConfigMsg(''); setNewProjModelParams({}); }}
|
|
1478
1507
|
style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: '#c5c5c5' }}
|
|
1479
1508
|
title="Novo Projeto..."
|
|
1480
1509
|
>
|
|
@@ -2194,6 +2223,22 @@ export default function App() {
|
|
|
2194
2223
|
</div>
|
|
2195
2224
|
</div>
|
|
2196
2225
|
|
|
2226
|
+
{/* Load refined model config */}
|
|
2227
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
|
|
2228
|
+
<button type="button" className="vscode-button" style={{ background: '#3c3c3c', fontSize: '12px' }}
|
|
2229
|
+
onClick={() => loadModelConfig(newProjPath, newProjModel, (cfg) => {
|
|
2230
|
+
if (cfg.model_params) setNewProjModelParams(cfg.model_params);
|
|
2231
|
+
if (cfg.model) setNewProjModel(cfg.model);
|
|
2232
|
+
})}>
|
|
2233
|
+
Load Refined Config
|
|
2234
|
+
</button>
|
|
2235
|
+
{modelConfigMsg && (
|
|
2236
|
+
<span style={{ fontSize: '11px', color: modelConfigMsg.startsWith('✅') ? '#4ec9b0' : '#f48771' }}>
|
|
2237
|
+
{modelConfigMsg}
|
|
2238
|
+
</span>
|
|
2239
|
+
)}
|
|
2240
|
+
</div>
|
|
2241
|
+
|
|
2197
2242
|
{newProjError && (
|
|
2198
2243
|
<div style={{ color: '#f48771', fontSize: '11px', marginTop: '4px', whiteSpace: 'pre-wrap' }}>
|
|
2199
2244
|
⚠️ {newProjError}
|
|
@@ -2359,6 +2404,25 @@ export default function App() {
|
|
|
2359
2404
|
</div>
|
|
2360
2405
|
</div>
|
|
2361
2406
|
|
|
2407
|
+
{/* Load refined model config */}
|
|
2408
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
|
|
2409
|
+
<button type="button" className="vscode-button" style={{ background: '#3c3c3c', fontSize: '12px' }}
|
|
2410
|
+
onClick={() => loadModelConfig(editingProject.project_path, editingProject.model, (cfg) => {
|
|
2411
|
+
setEditingProject(p => ({
|
|
2412
|
+
...p,
|
|
2413
|
+
model_params: cfg.model_params || p.model_params,
|
|
2414
|
+
model: cfg.model || p.model,
|
|
2415
|
+
}));
|
|
2416
|
+
})}>
|
|
2417
|
+
Load Refined Config
|
|
2418
|
+
</button>
|
|
2419
|
+
{modelConfigMsg && (
|
|
2420
|
+
<span style={{ fontSize: '11px', color: modelConfigMsg.startsWith('✅') ? '#4ec9b0' : '#f48771' }}>
|
|
2421
|
+
{modelConfigMsg}
|
|
2422
|
+
</span>
|
|
2423
|
+
)}
|
|
2424
|
+
</div>
|
|
2425
|
+
|
|
2362
2426
|
{/* Alternative model */}
|
|
2363
2427
|
<div className="flex flex-col" style={{ gap: '4px' }}>
|
|
2364
2428
|
<label className="vscode-sidebar-section-title" style={{ padding: 0 }}>Modelo Alternativo</label>
|
|
@@ -92,6 +92,11 @@ input, select, textarea {
|
|
|
92
92
|
outline: none;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
select option {
|
|
96
|
+
background-color: var(--vscode-input-bg);
|
|
97
|
+
color: var(--vscode-text-fg);
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
input:focus, select:focus, textarea:focus {
|
|
96
101
|
border-color: var(--vscode-active-border);
|
|
97
102
|
}
|
|
Binary file
|
|
@@ -286,11 +286,22 @@ async def handle_slash_command(data: dict) -> dict:
|
|
|
286
286
|
T.confirm = lambda p, default=True: default # sync fallback never used in GUI
|
|
287
287
|
T.ask = lambda p: ""
|
|
288
288
|
|
|
289
|
+
def _render_rich(obj) -> str:
|
|
290
|
+
from rich.console import Console as _Console
|
|
291
|
+
from rich.table import Table as _Table
|
|
292
|
+
from io import StringIO
|
|
293
|
+
if isinstance(obj, _Table):
|
|
294
|
+
buf = StringIO()
|
|
295
|
+
c = _Console(file=buf, highlight=False, no_color=True)
|
|
296
|
+
c.print(obj)
|
|
297
|
+
return buf.getvalue()
|
|
298
|
+
return str(obj)
|
|
299
|
+
|
|
289
300
|
def _console_print(*args_c, **kwargs_c):
|
|
290
301
|
if not args_c:
|
|
291
302
|
messages.append("")
|
|
292
303
|
return
|
|
293
|
-
raw = " ".join(
|
|
304
|
+
raw = " ".join(_render_rich(x) for x in args_c)
|
|
294
305
|
for line in raw.split("\n"):
|
|
295
306
|
stripped = line.strip()
|
|
296
307
|
if not stripped:
|
|
@@ -302,6 +313,9 @@ async def handle_slash_command(data: dict) -> dict:
|
|
|
302
313
|
messages.append("### 🧠 Active skills for this project\n"); continue
|
|
303
314
|
if "Available skills" in stripped:
|
|
304
315
|
messages.append("### 📚 Available skills\n"); continue
|
|
316
|
+
clean = re.sub(r'\[/?[\w\s]+\]', '', stripped)
|
|
317
|
+
if clean and clean == clean.upper() and clean.replace(" ", "").isalpha():
|
|
318
|
+
messages.append(f"### {clean}\n"); continue
|
|
305
319
|
m = re.match(r'\s*(?:\*\s*)?\[(green|cyan)\]\s*(.*?)\s*\[/\1\]\s*(.*)', line)
|
|
306
320
|
if m:
|
|
307
321
|
name = m.group(2).strip()
|
|
@@ -316,7 +330,7 @@ async def handle_slash_command(data: dict) -> dict:
|
|
|
316
330
|
if sm:
|
|
317
331
|
star = "⭐ " if has_star else "🔹 "
|
|
318
332
|
messages.append(f"{star}**`{sm.group(1).strip()}`** — {sm.group(2).strip()}"); continue
|
|
319
|
-
messages.append(re.sub(r'\[
|
|
333
|
+
messages.append(re.sub(r'\[/?[\w\s]+\]', '', line))
|
|
320
334
|
|
|
321
335
|
T.console.print = _console_print
|
|
322
336
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""AssetStore — local repository of reusable assets (skills and model configs).
|
|
2
|
+
|
|
3
|
+
Structure
|
|
4
|
+
---------
|
|
5
|
+
opalacoder/assetstore/
|
|
6
|
+
skills/
|
|
7
|
+
<ID>.zip — full skill directory tree
|
|
8
|
+
<ID>.metadata — YAML: id, type, name, desc
|
|
9
|
+
modelconfigs/
|
|
10
|
+
<ID>.zip — single YAML file for the model config
|
|
11
|
+
<ID>.metadata — YAML: id, type, desc, model
|
|
12
|
+
|
|
13
|
+
Installation targets (relative to project root)
|
|
14
|
+
---------
|
|
15
|
+
skill → <project>/.opalacoder/skills/<name>/
|
|
16
|
+
modelconfig → <project>/.opalacoder/modelsconfig/<provider>/<model_file>.yaml
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import zipfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
# Root of the assetstore bundled with the package
|
|
27
|
+
_STORE_ROOT = Path(__file__).parent / "assetstore"
|
|
28
|
+
|
|
29
|
+
VALID_TYPES = {"skill", "modelconfig"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Internal helpers
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _store_dir(asset_type: str) -> Path:
|
|
37
|
+
return _STORE_ROOT / (asset_type + "s") # skills/ or modelconfigs/
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_metadata(path: Path) -> dict:
|
|
41
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
42
|
+
return yaml.safe_load(f) or {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _iter_assets(asset_type: str) -> list[dict]:
|
|
46
|
+
"""Return list of metadata dicts for all assets of the given type."""
|
|
47
|
+
d = _store_dir(asset_type)
|
|
48
|
+
if not d.exists():
|
|
49
|
+
return []
|
|
50
|
+
results = []
|
|
51
|
+
for meta_file in sorted(d.glob("*.metadata")):
|
|
52
|
+
try:
|
|
53
|
+
meta = _parse_metadata(meta_file)
|
|
54
|
+
meta["_zip"] = meta_file.with_suffix(".zip")
|
|
55
|
+
meta["_meta"] = meta_file
|
|
56
|
+
results.append(meta)
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
59
|
+
return results
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _match(meta: dict, desc: str) -> bool:
|
|
63
|
+
"""True if desc matches the asset's id or desc field (case-insensitive)."""
|
|
64
|
+
desc_l = desc.lower()
|
|
65
|
+
return (
|
|
66
|
+
meta.get("id", "").lower() == desc_l
|
|
67
|
+
or meta.get("desc", "").lower() == desc_l
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _model_to_path(model: str) -> tuple[str, str]:
|
|
72
|
+
"""'ollama/gpt-oss:latest' → ('ollama', 'gpt-oss__latest.yaml')"""
|
|
73
|
+
_ALIASES = {"ollama_chat": "ollama"}
|
|
74
|
+
if "/" in model:
|
|
75
|
+
raw_provider, model_name = model.split("/", 1)
|
|
76
|
+
else:
|
|
77
|
+
raw_provider, model_name = "", model
|
|
78
|
+
provider = _ALIASES.get(raw_provider, raw_provider)
|
|
79
|
+
filename = model_name.replace(":", "__") + ".yaml"
|
|
80
|
+
return provider, filename
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Public API
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def list_assets(asset_type: Optional[str] = None) -> list[dict]:
|
|
88
|
+
"""Return all assets, optionally filtered by type."""
|
|
89
|
+
types = [asset_type] if asset_type else list(VALID_TYPES)
|
|
90
|
+
result = []
|
|
91
|
+
for t in types:
|
|
92
|
+
result.extend(_iter_assets(t))
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def find_assets(asset_type: str, desc: str) -> list[dict]:
|
|
97
|
+
"""Return matching assets. desc='*' returns all of the type."""
|
|
98
|
+
assets = _iter_assets(asset_type)
|
|
99
|
+
if desc == "*":
|
|
100
|
+
return assets
|
|
101
|
+
return [a for a in assets if _match(a, desc)]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def install_asset(meta: dict, project_path: str) -> str:
|
|
105
|
+
"""Extract asset zip into the correct location inside project_path.
|
|
106
|
+
|
|
107
|
+
Returns a human-readable summary of what was installed.
|
|
108
|
+
"""
|
|
109
|
+
zip_path: Path = meta["_zip"]
|
|
110
|
+
if not zip_path.exists():
|
|
111
|
+
raise FileNotFoundError(f"Zip not found: {zip_path}")
|
|
112
|
+
|
|
113
|
+
asset_type = meta.get("type", "")
|
|
114
|
+
project = Path(os.path.abspath(project_path))
|
|
115
|
+
|
|
116
|
+
if asset_type == "skill":
|
|
117
|
+
dest = project / ".opalacoder" / "skills"
|
|
118
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
120
|
+
zf.extractall(dest)
|
|
121
|
+
skill_name = meta.get("name", zip_path.stem)
|
|
122
|
+
return f"skill '{skill_name}' installed at {dest / skill_name}"
|
|
123
|
+
|
|
124
|
+
elif asset_type == "modelconfig":
|
|
125
|
+
model = meta.get("model", "")
|
|
126
|
+
if not model:
|
|
127
|
+
raise ValueError(f"Asset {meta.get('id')} has no 'model' field in metadata")
|
|
128
|
+
provider, filename = _model_to_path(model)
|
|
129
|
+
dest_dir = project / ".opalacoder" / "modelsconfig" / provider
|
|
130
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
132
|
+
names = zf.namelist()
|
|
133
|
+
if len(names) != 1:
|
|
134
|
+
raise ValueError(f"modelconfig zip must contain exactly one file, got: {names}")
|
|
135
|
+
zf.extractall(dest_dir)
|
|
136
|
+
# Rename to canonical model filename
|
|
137
|
+
extracted = dest_dir / names[0]
|
|
138
|
+
target = dest_dir / filename
|
|
139
|
+
if extracted != target:
|
|
140
|
+
extracted.rename(target)
|
|
141
|
+
return f"modelconfig for '{model}' installed at {target}"
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f"Unknown asset type '{asset_type}'")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def register_asset(asset_type: str, source_path: str, metadata: dict) -> Path:
|
|
148
|
+
"""Package a local directory/file as an asset and register it in the store.
|
|
149
|
+
|
|
150
|
+
source_path: directory (for skill) or .yaml file (for modelconfig)
|
|
151
|
+
metadata: dict with at least id, type, desc, and name or model
|
|
152
|
+
Returns the path of the created zip.
|
|
153
|
+
"""
|
|
154
|
+
if asset_type not in VALID_TYPES:
|
|
155
|
+
raise ValueError(f"type must be one of {VALID_TYPES}")
|
|
156
|
+
|
|
157
|
+
asset_id = metadata.get("id")
|
|
158
|
+
if not asset_id:
|
|
159
|
+
raise ValueError("metadata must have an 'id' field")
|
|
160
|
+
|
|
161
|
+
store_dir = _store_dir(asset_type)
|
|
162
|
+
store_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
zip_path = store_dir / f"{asset_id}.zip"
|
|
165
|
+
meta_path = store_dir / f"{asset_id}.metadata"
|
|
166
|
+
|
|
167
|
+
source = Path(source_path)
|
|
168
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
169
|
+
if source.is_dir():
|
|
170
|
+
for f in sorted(source.rglob("*")):
|
|
171
|
+
if f.is_file() and "__pycache__" not in str(f) and not f.name.endswith(".pyc"):
|
|
172
|
+
zf.write(f, f.relative_to(source.parent))
|
|
173
|
+
else:
|
|
174
|
+
zf.write(source, source.name)
|
|
175
|
+
|
|
176
|
+
with open(meta_path, "w", encoding="utf-8") as f:
|
|
177
|
+
yaml.dump(metadata, f, allow_unicode=True, default_flow_style=False)
|
|
178
|
+
|
|
179
|
+
return zip_path
|