multacd 1.0.0__tar.gz → 2.0.0b2__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.
- {multacd-1.0.0 → multacd-2.0.0b2}/.github/workflows/release.yml +14 -3
- {multacd-1.0.0 → multacd-2.0.0b2}/CHANGELOG.md +29 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/DESIGN.md +23 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/PKG-INFO +2 -3
- {multacd-1.0.0 → multacd-2.0.0b2}/README.md +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/core/agent_loop.py +129 -16
- multacd-2.0.0b2/core/clipboard.py +74 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/config.py +12 -12
- multacd-2.0.0b2/core/llm_client.py +367 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/mode_manager.py +13 -13
- {multacd-1.0.0 → multacd-2.0.0b2}/core/permissions.py +27 -3
- multacd-2.0.0b2/core/providers/__init__.py +137 -0
- multacd-2.0.0b2/core/providers/__main__.py +38 -0
- multacd-2.0.0b2/core/providers/anthropic.py +329 -0
- multacd-2.0.0b2/core/providers/openai_compat.py +301 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/version.py +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/daemon/process.py +7 -7
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/configuration.md +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/index.md +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/main.py +12 -12
- {multacd-1.0.0 → multacd-2.0.0b2}/multacd.spec +8 -23
- {multacd-1.0.0 → multacd-2.0.0b2}/plan/PLAN-phase2.md +4 -4
- {multacd-1.0.0 → multacd-2.0.0b2}/pyproject.toml +3 -2
- {multacd-1.0.0 → multacd-2.0.0b2}/requirements.txt +0 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/roadmap/ROADMAP.md +8 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/scheduler/engine.py +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/scheduler/jobs/__init__.py +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/scripts/install.sh +45 -9
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_binary.py +3 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_entrypoint.py +3 -4
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/code/run_python.py +17 -20
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/code/run_tests.py +12 -10
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_push.py +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/registry.py +21 -3
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/shell/bash.py +12 -17
- multacd-2.0.0b2/tools/streaming.py +142 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/app.py +1 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/icons.py +25 -6
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/screens/main_screen.py +249 -17
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/screens/setup_wizard.py +156 -29
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/themes/__init__.py +91 -14
- multacd-2.0.0b2/tui/themes/__main__.py +26 -0
- multacd-2.0.0b2/tui/tokens.py +90 -0
- multacd-2.0.0b2/tui/widgets/chat_panel.py +182 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/confirm_dialog.py +8 -4
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/diff_viewer.py +15 -3
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/file_tree.py +4 -4
- multacd-2.0.0b2/tui/widgets/info_panel.py +87 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/input_bar.py +35 -2
- multacd-2.0.0b2/tui/widgets/model_selector.py +335 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/permission_popup.py +83 -16
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/slash_palette.py +29 -4
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/sources_panel.py +5 -2
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/status_bar.py +22 -7
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/thinking_bar.py +16 -1
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/tool_activity.py +11 -6
- multacd-1.0.0/core/llm_client.py +0 -347
- multacd-1.0.0/plan/PLAN-phase3.md +0 -1103
- multacd-1.0.0/plan/PLAN-phase4.md +0 -1326
- multacd-1.0.0/plan/PLAN-phase5.md +0 -1334
- multacd-1.0.0/tui/themes/__main__.py +0 -17
- multacd-1.0.0/tui/widgets/chat_panel.py +0 -83
- {multacd-1.0.0 → multacd-2.0.0b2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/.github/pull_request_template.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/.github/workflows/test.yml +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/.gitignore +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/CONTRIBUTING.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/LICENSE +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/generator.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/privacy.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/sources/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/sources/git_source.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/sources/news_source.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/briefing/sources/todo_source.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/codebase.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/plugin_loader.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/prompt_composer.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/research/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/research/bus.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/research/orchestrator.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/research/query_generator.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/core/updater.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/daemon/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/daemon/ipc.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/install.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/mkdocs.yml +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/modes/coding.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/modes/personal.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/modes/research.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/plugins.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/docs/quickstart.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/memory/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/memory/context.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/memory/store.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/plan/PLAN-phase1.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/plugins/README.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/plugins/example_plugin.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/scheduler/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/scheduler/jobs/briefing_job.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/scheduler/jobs/research_job.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/scripts/install.ps1 +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/__main__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/base.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/brave.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/duckduckgo.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/exa.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/serpapi.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/search_providers/tavily.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/soul.md +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/conftest.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/integration/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/integration/test_agent_loop.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/test_selftests.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_config.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_docs.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_github.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_installer.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_permissions.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_plugin_loader.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_privacy_filter.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_search_providers/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_search_providers/test_tavily.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_tools/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_tools/test_bash.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_tools/test_read_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_tools/test_web_scrape.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_tools/test_write_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tests/unit/test_updater.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/access_control.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/agent.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/bot.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/file_handler.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/formatter.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tg/handlers.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/agent/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/agent/ask.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/agent/skill.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/agent/task.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/agent/todo_write.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/code/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/code/lint_python.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/codebase/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/codebase/scan_codebase.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/common.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/apply_patch.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/delete_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/edit_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/glob.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/grep.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/list_dir.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/move_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/multi_edit.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/read_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/read_many_files.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/filesystem/write_file.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/_helper.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_add.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_branch.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_checkout.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_commit.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_diff.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_log.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_merge.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_pull.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/git/git_status.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/memory/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/memory/forget.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/memory/recall.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/memory/remember.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/cancel_job.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/daemon_status.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/generate_briefing.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/get_jobs.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/schedule_job.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/send_telegram.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/personal/user_manager.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/research/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/research/deep_research.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/research/export_research.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/research/quick_research.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/research/web_scrape.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/research/web_search.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/shell/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/web/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tools/web/web_fetch.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/screens/__init__.py +0 -0
- {multacd-1.0.0 → multacd-2.0.0b2}/tui/widgets/__init__.py +0 -0
|
@@ -76,7 +76,9 @@ jobs:
|
|
|
76
76
|
path: dist_bin/
|
|
77
77
|
- run: pip install build twine
|
|
78
78
|
- run: python -m build --outdir dist_pkg/
|
|
79
|
-
-
|
|
79
|
+
# --skip-existing: rerun aman (file yang sudah naik di-skip, bukan error).
|
|
80
|
+
# --verbose: body error PyPI kelihatan di log kalau 4xx lagi.
|
|
81
|
+
- run: twine upload --skip-existing --verbose dist_pkg/*
|
|
80
82
|
env:
|
|
81
83
|
TWINE_USERNAME: __token__
|
|
82
84
|
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -84,13 +86,22 @@ jobs:
|
|
|
84
86
|
with:
|
|
85
87
|
files: dist_bin/**/*
|
|
86
88
|
generate_release_notes: true
|
|
89
|
+
# Tag beta (v2.0.0b1 / vX-rc) → prerelease, bukan final.
|
|
90
|
+
prerelease: ${{ contains(github.ref_name, 'b') || contains(github.ref_name, '-') }}
|
|
87
91
|
body: |
|
|
88
92
|
## Install
|
|
89
93
|
|
|
90
|
-
Linux / macOS
|
|
94
|
+
Linux / macOS (download dulu biar gagalnya kelihatan):
|
|
91
95
|
|
|
92
96
|
```bash
|
|
93
|
-
curl -
|
|
97
|
+
curl -fSL https://raw.githubusercontent.com/prototypeall850-creator/multacd/main/scripts/install.sh -o ./m-install.sh && bash ./m-install.sh && rm ./m-install.sh
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Termux (binary rilis tidak kompatibel — pakai pip):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pkg install -y python git
|
|
104
|
+
pip install "multacd==2.0.0b2" --pre
|
|
94
105
|
```
|
|
95
106
|
|
|
96
107
|
Windows (PowerShell):
|
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
Format: Keep a Changelog. Versi: Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## [2.0.0b2] - 2026-09-12
|
|
6
|
+
|
|
7
|
+
Beta v2 kedua: cabut LiteLLM, ganti provider native (httpx, 2 adapter
|
|
8
|
+
SSE: OpenAI-compatible + Anthropic) — install pip Termux ringan tanpa
|
|
9
|
+
Rust, binary susut 91MB ke 56MB. Cost estimasi lokal ala OpenCode
|
|
10
|
+
tampil di info panel. Interface stream_completion tidak berubah.
|
|
11
|
+
|
|
12
|
+
Install beta: `pip install --pre "multacd==2.0.0b2"` (PyPI pre-release)
|
|
13
|
+
atau binary dari halaman GitHub prerelease.
|
|
14
|
+
|
|
15
|
+
## [2.0.0b1] - 2026-09-12
|
|
16
|
+
|
|
17
|
+
Beta v2 buat test Termux: redesign TUI (tanpa emoticon, responsif),
|
|
18
|
+
model selector, info panel, live output, token resmi, approve commit,
|
|
19
|
+
wizard back-nav, /copy, installer anti-diam + panduan pip Termux.
|
|
20
|
+
|
|
21
|
+
Install beta: `pip install --pre "multacd==2.0.0b1"` (PyPI pre-release)
|
|
22
|
+
atau binary dari halaman GitHub prerelease.
|
|
23
|
+
|
|
24
|
+
## [Unreleased] (menuju 2.0.0 — redesign v2 full bebas)
|
|
25
|
+
|
|
26
|
+
### Changed (breaking visual, config v1 tetap kebaca)
|
|
27
|
+
- Tema default `multacd-dark` + `multacd-light`/`multacd-min` (Termux hemat)
|
|
28
|
+
- Status bar compact di layar sempit, splash v2, palette fuzzy, thinking statis min-mode
|
|
29
|
+
- Wizard numbering /6, AskDialog responsif, marker git peach
|
|
30
|
+
- Wizard back-nav (Esc/tombol, fetch basi dibuang), tree reload tiap turn
|
|
31
|
+
- `/copy [n]` + Ctrl+Y salin jawaban (clipboard chain + fallback file)
|
|
32
|
+
- Live output tool, token resmi provider, approve commit + branch dialog
|
|
33
|
+
|
|
5
34
|
## [1.0.0] - 2026-09-11
|
|
6
35
|
|
|
7
36
|
Rilis stabil pertama: coding + research + personal agent dalam satu TUI,
|
|
@@ -990,3 +990,26 @@ Section Keputusan Mulai berlaku
|
|
|
990
990
|
11 Info panel kanan (Ctrl+I) Phase 2 TUI
|
|
991
991
|
12 Model selector popup Phase 2 TUI
|
|
992
992
|
13 Permission bar (inline, 3 brs) Phase 2 TUI (ganti Section 4)
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
## 14. Redesign v2 (full bebas, Termux baseline v0.119)
|
|
997
|
+
|
|
998
|
+
Breaking visual, kompatibel config (config v1 tetap kebaca):
|
|
999
|
+
|
|
1000
|
+
- Tema default = multacd-dark (identitas sendiri, turunan Mocha dengan
|
|
1001
|
+
aksen sapphire + kontras naik buat HP). Catppuccin 4 varian tetap ada.
|
|
1002
|
+
Tambah multacd-light + multacd-min (16 warna sistem, Termux hemat).
|
|
1003
|
+
Token semantik di tui/tokens.py — widget dilarang hardcode hex.
|
|
1004
|
+
- Icon unicode tanpa emoji 2-cell. Auto jadi ascii kalau
|
|
1005
|
+
NO_EMOJI/NO_COLOR/Termux sempit (<70 kolom).
|
|
1006
|
+
- Status bar responsif: <70 kolom tampil 'multacd - mode - status' saja.
|
|
1007
|
+
Bukan repo = segmen git hilang (bukan 'no git').
|
|
1008
|
+
- Thinking statis saat MULTACD_MIN=1/NO_COLOR (tanpa repaint 200ms).
|
|
1009
|
+
- Slash palette fuzzy berperingkat: '/md' ketemu '/model'.
|
|
1010
|
+
- Tool header berwarna (run biru/sukses hijau/gagal merah).
|
|
1011
|
+
- Splash v2: logo + mode-model-versi + 1 tip. Welcome ringkas 2 baris.
|
|
1012
|
+
- Panel kanan min-width 28 ke 20. Git watcher 60 dtk saat min-mode.
|
|
1013
|
+
- AskDialog responsif (80% max 60) + ikon tanpa emoji.
|
|
1014
|
+
- Wizard numbering /6 (sebelumnya campur /5 dan /6).
|
|
1015
|
+
- File tree marker peach #fab387.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: multacd
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b2
|
|
4
4
|
Summary: Agentic TUI for coding, research, and personal automation
|
|
5
5
|
Project-URL: Homepage, https://github.com/prototypeall850-creator/multacd
|
|
6
6
|
Project-URL: Issues, https://github.com/prototypeall850-creator/multacd/issues
|
|
@@ -20,7 +20,6 @@ Requires-Dist: apscheduler>=3.10.0
|
|
|
20
20
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
21
21
|
Requires-Dist: exa-py>=1.0.0
|
|
22
22
|
Requires-Dist: httpx>=0.27.0
|
|
23
|
-
Requires-Dist: litellm>=1.40.0
|
|
24
23
|
Requires-Dist: markdownify>=0.12.0
|
|
25
24
|
Requires-Dist: pydantic>=2.0
|
|
26
25
|
Requires-Dist: python-telegram-bot>=21.0
|
|
@@ -38,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
38
37
|
# ⚡ multacd
|
|
39
38
|
|
|
40
39
|
Agentic TUI untuk coding, research, dan personal automation — dalam satu terminal.
|
|
41
|
-
Satu config BYOK untuk
|
|
40
|
+
Satu config BYOK untuk LLM favoritmu via provider native (ringan, Termux aman).
|
|
42
41
|
|
|
43
42
|
```
|
|
44
43
|
> /code baca main.py lalu jelaskan cara kerjanya
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ⚡ multacd
|
|
2
2
|
|
|
3
3
|
Agentic TUI untuk coding, research, dan personal automation — dalam satu terminal.
|
|
4
|
-
Satu config BYOK untuk
|
|
4
|
+
Satu config BYOK untuk LLM favoritmu via provider native (ringan, Termux aman).
|
|
5
5
|
|
|
6
6
|
```
|
|
7
7
|
> /code baca main.py lalu jelaskan cara kerjanya
|
|
@@ -5,8 +5,8 @@ Async generator — setiap update di-yield real-time ke TUI:
|
|
|
5
5
|
|
|
6
6
|
async for event in run_agent(user_input, context, config):
|
|
7
7
|
isinstance(event, AgentText) → tampilkan potongan teks
|
|
8
|
-
isinstance(event, AgentToolStart) → tampilkan
|
|
9
|
-
isinstance(event, AgentToolDone) → update jadi
|
|
8
|
+
isinstance(event, AgentToolStart) → tampilkan tool berjalan
|
|
9
|
+
isinstance(event, AgentToolDone) → update jadi sukses / gagal
|
|
10
10
|
isinstance(event, AgentDone) → agent selesai
|
|
11
11
|
isinstance(event, AgentError) → tampilkan error, loop berhenti
|
|
12
12
|
|
|
@@ -49,6 +49,9 @@ SYSTEM_PROMPT = (
|
|
|
49
49
|
# confirm: "yes" | "no" | "all" (all = izinkan semua sesi ini)
|
|
50
50
|
ConfirmCallback = Callable[[str, dict[str, Any]], Awaitable[str]]
|
|
51
51
|
AskCallback = Callable[[str], Awaitable[str]]
|
|
52
|
+
# Live output tool: (call_id, baris). Dipanggil dari thread worker tool —
|
|
53
|
+
# TUI wajib marshal (call_from_thread); CLI boleh print langsung.
|
|
54
|
+
OutputCallback = Callable[[str, str], None]
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
@dataclass
|
|
@@ -76,29 +79,52 @@ class AgentDone:
|
|
|
76
79
|
text: str
|
|
77
80
|
|
|
78
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class AgentUsage:
|
|
84
|
+
"""Token resmi provider untuk satu panggilan LLM (0 = tak dilapor)."""
|
|
85
|
+
|
|
86
|
+
prompt_tokens: int = 0
|
|
87
|
+
completion_tokens: int = 0
|
|
88
|
+
cost_usd: float = 0.0 # estimasi lokal (tabel harga provider)
|
|
89
|
+
|
|
90
|
+
|
|
79
91
|
@dataclass
|
|
80
92
|
class AgentError:
|
|
81
93
|
message: str
|
|
82
94
|
|
|
83
95
|
|
|
84
|
-
AgentEvent = AgentText | AgentToolStart | AgentToolDone | AgentDone | AgentError
|
|
96
|
+
AgentEvent = AgentText | AgentToolStart | AgentToolDone | AgentDone | AgentUsage | AgentError
|
|
85
97
|
|
|
86
98
|
|
|
87
99
|
async def _stdin_confirm(tool_name: str, params: dict[str, Any]) -> str:
|
|
88
100
|
target = params.get("path") or params.get("command") or params.get("url") or ""
|
|
89
|
-
|
|
101
|
+
extra = ""
|
|
102
|
+
if tool_name == "git_commit":
|
|
103
|
+
extra = "/e edit pesan"
|
|
104
|
+
elif tool_name == "git_push":
|
|
105
|
+
extra = "/b branch baru"
|
|
106
|
+
print(f"[izin] {tool_name} {target} — lanjutkan? [y/n/a{extra}] ", end="", flush=True)
|
|
90
107
|
try:
|
|
91
108
|
ans = await asyncio.to_thread(input)
|
|
92
109
|
except (EOFError, KeyboardInterrupt):
|
|
93
110
|
return "no"
|
|
94
|
-
ans = ans.strip()
|
|
111
|
+
ans = ans.strip()
|
|
95
112
|
if ans in ("a", "all"):
|
|
96
113
|
return "all"
|
|
114
|
+
if ans in ("e", "edit") and tool_name == "git_commit":
|
|
115
|
+
try:
|
|
116
|
+
new_msg = await asyncio.to_thread(
|
|
117
|
+
input, f"Pesan baru [{params.get('message', '')}]: ")
|
|
118
|
+
except (EOFError, KeyboardInterrupt):
|
|
119
|
+
return "no"
|
|
120
|
+
return f"edit:{new_msg.strip() or params.get('message', '')}"
|
|
121
|
+
if ans in ("b", "branch") and tool_name == "git_push":
|
|
122
|
+
return "branch"
|
|
97
123
|
return "yes" if ans in ("y", "yes") else "no"
|
|
98
124
|
|
|
99
125
|
|
|
100
126
|
async def _stdin_ask(question: str) -> str:
|
|
101
|
-
print(f"
|
|
127
|
+
print(f"? {question}")
|
|
102
128
|
try:
|
|
103
129
|
return await asyncio.to_thread(input, "Jawaban: ")
|
|
104
130
|
except (EOFError, KeyboardInterrupt):
|
|
@@ -116,6 +142,7 @@ async def run_agent(
|
|
|
116
142
|
mode_manager: ModeManager | None = None,
|
|
117
143
|
active_tools: list[str] | None = None,
|
|
118
144
|
composer: PromptComposer | None = None,
|
|
145
|
+
output_cb: OutputCallback | None = None,
|
|
119
146
|
) -> AsyncIterator[AgentEvent]:
|
|
120
147
|
"""Jalankan satu turn agent. Yield AgentEvent secara real-time."""
|
|
121
148
|
def _system_prompt() -> str:
|
|
@@ -154,7 +181,7 @@ async def run_agent(
|
|
|
154
181
|
iteration += 1
|
|
155
182
|
if iteration > config.max_tool_iterations:
|
|
156
183
|
yield AgentError(
|
|
157
|
-
f"
|
|
184
|
+
f"Mencapai batas {config.max_tool_iterations} iterasi tool. "
|
|
158
185
|
"Berhenti agar tidak infinite loop — coba pecah tugas jadi langkah kecil."
|
|
159
186
|
)
|
|
160
187
|
return
|
|
@@ -168,11 +195,12 @@ async def run_agent(
|
|
|
168
195
|
yield AgentText(event.content)
|
|
169
196
|
elif isinstance(event, StreamDone):
|
|
170
197
|
done = event
|
|
198
|
+
yield AgentUsage(done.prompt_tokens, done.completion_tokens)
|
|
171
199
|
if done is None:
|
|
172
200
|
# Provider aneh: stream tutup tanpa StreamDone — jangan
|
|
173
201
|
# biarkan assert mentah, kasih pesan yang bisa dibaca user.
|
|
174
202
|
yield AgentError(
|
|
175
|
-
"
|
|
203
|
+
"Provider menutup stream tanpa hasil lengkap. "
|
|
176
204
|
"Coba ulangi; kalau berulang, cek status provider / model.")
|
|
177
205
|
return
|
|
178
206
|
except LLMError as e:
|
|
@@ -211,6 +239,36 @@ async def run_agent(
|
|
|
211
239
|
if choice == "all":
|
|
212
240
|
checker.approve_all_for_session(call.name)
|
|
213
241
|
choice = "yes"
|
|
242
|
+
# #4: user edit pesan commit → pakai pesan baru, lanjut yes.
|
|
243
|
+
if choice.startswith("edit:"):
|
|
244
|
+
new_msg = choice[len("edit:"):].strip()
|
|
245
|
+
if not new_msg:
|
|
246
|
+
choice = "no" # edit dibatalkan = tolak
|
|
247
|
+
elif "message" in call.arguments:
|
|
248
|
+
call.arguments["message"] = new_msg
|
|
249
|
+
choice = "yes"
|
|
250
|
+
else:
|
|
251
|
+
choice = "yes"
|
|
252
|
+
# #4: user pilih buat branch baru (push protected) → buat
|
|
253
|
+
# branch, arahkan push ke sana, lanjut yes.
|
|
254
|
+
if choice == "branch":
|
|
255
|
+
new_branch = (await ask_cb(
|
|
256
|
+
"Nama branch baru buat push (kosongkan = batal)?"
|
|
257
|
+
) or "").strip()
|
|
258
|
+
if not new_branch:
|
|
259
|
+
choice = "no"
|
|
260
|
+
else:
|
|
261
|
+
made = await asyncio.to_thread(
|
|
262
|
+
execute_tool, "git_checkout",
|
|
263
|
+
{"workdir": call.arguments.get("workdir", "."),
|
|
264
|
+
"branch": new_branch, "create": True})
|
|
265
|
+
if not made.get("success"):
|
|
266
|
+
context.add_tool_result(call.id, call.name, made)
|
|
267
|
+
yield AgentToolDone(call.id, call.name, False, made)
|
|
268
|
+
continue
|
|
269
|
+
call.arguments["branch"] = new_branch
|
|
270
|
+
call.arguments.pop("allow_protected", None)
|
|
271
|
+
choice = "yes"
|
|
214
272
|
if choice != "yes":
|
|
215
273
|
cancel_result = fail(
|
|
216
274
|
f"Dibatalkan user — jangan coba {call.name} yang sama lagi, "
|
|
@@ -222,7 +280,13 @@ async def run_agent(
|
|
|
222
280
|
# to_thread: tool sync jalan di thread, event loop tetap hidup.
|
|
223
281
|
# Tanpa ini, tool research (yang emit event via call_from_thread)
|
|
224
282
|
# deadlock — loop diblok nunggu thread, thread nunggu loop.
|
|
225
|
-
|
|
283
|
+
# output_cb (live stream) jalan dari thread itu juga.
|
|
284
|
+
def _live(line: str, _cid: str = call.id) -> None:
|
|
285
|
+
assert output_cb is not None
|
|
286
|
+
output_cb(_cid, line)
|
|
287
|
+
result = await asyncio.to_thread(
|
|
288
|
+
execute_tool, call.name, call.arguments,
|
|
289
|
+
_live if output_cb is not None else None)
|
|
226
290
|
context.add_tool_result(call.id, call.name, result)
|
|
227
291
|
yield AgentToolDone(call.id, call.name, result["success"], result)
|
|
228
292
|
|
|
@@ -270,9 +334,10 @@ if __name__ == "__main__":
|
|
|
270
334
|
events = await _drain(run_agent("ls", ctx, cfg, llm_client=fake,
|
|
271
335
|
confirm=_no_confirm))
|
|
272
336
|
kinds = [type(e).__name__ for e in events]
|
|
273
|
-
assert kinds == ["AgentText", "AgentText", "
|
|
274
|
-
"AgentText", "
|
|
275
|
-
|
|
337
|
+
assert kinds == ["AgentText", "AgentText", "AgentUsage", "AgentToolStart",
|
|
338
|
+
"AgentToolDone", "AgentText", "AgentUsage",
|
|
339
|
+
"AgentDone"], kinds
|
|
340
|
+
assert events[4].success is True
|
|
276
341
|
tool_msgs = [m for m in ctx.get_messages() if m["role"] == "tool"]
|
|
277
342
|
assert len(tool_msgs) == 1 and tool_msgs[0]["tool_call_id"] == "c1"
|
|
278
343
|
# assistant message membawa tool_calls format OpenAI
|
|
@@ -289,7 +354,7 @@ if __name__ == "__main__":
|
|
|
289
354
|
])
|
|
290
355
|
ctx = ConversationContext()
|
|
291
356
|
events = await _drain(run_agent("tulis", ctx, cfg, llm_client=fake, confirm=_deny))
|
|
292
|
-
assert events[
|
|
357
|
+
assert events[1].name == "write_file" and events[2].success is False
|
|
293
358
|
assert "Dibatalkan user" in ctx.get_messages()[-2]["content"]
|
|
294
359
|
|
|
295
360
|
# 4. "all" = sesi ini auto seterusnya
|
|
@@ -315,7 +380,7 @@ if __name__ == "__main__":
|
|
|
315
380
|
])
|
|
316
381
|
ctx = ConversationContext()
|
|
317
382
|
events = await _drain(run_agent("x", ctx, cfg, llm_client=fake, confirm=_no_confirm))
|
|
318
|
-
assert events[
|
|
383
|
+
assert events[2].success is False
|
|
319
384
|
assert "tidak tersedia" in ctx.get_messages()[-2]["content"]
|
|
320
385
|
assert isinstance(events[-1], AgentDone)
|
|
321
386
|
|
|
@@ -339,7 +404,7 @@ if __name__ == "__main__":
|
|
|
339
404
|
ctx = ConversationContext()
|
|
340
405
|
events = await _drain(run_agent("tanya", ctx, cfg, llm_client=fake,
|
|
341
406
|
confirm=_no_confirm, ask_user=_answer))
|
|
342
|
-
assert events[
|
|
407
|
+
assert events[2].success is True
|
|
343
408
|
assert "budi" in ctx.get_messages()[-2]["content"]
|
|
344
409
|
|
|
345
410
|
# 8. Slash command dicegat — LLM tidak dipanggil, history bersih
|
|
@@ -402,7 +467,55 @@ if __name__ == "__main__":
|
|
|
402
467
|
assert ctx.get_messages()[0]["content"].startswith("SOULKU"), \
|
|
403
468
|
ctx.get_messages()[0]["content"][:100]
|
|
404
469
|
|
|
405
|
-
|
|
470
|
+
# 11. #4 edit pesan commit: confirm "edit:X" → commit pakai X
|
|
471
|
+
import subprocess as _sp
|
|
472
|
+
import tempfile as _tf
|
|
473
|
+
from pathlib import Path as _Path
|
|
474
|
+
tmp = _Path(_tf.mkdtemp(prefix="multacd-commit-"))
|
|
475
|
+
_sp.run(["git", "-C", str(tmp), "init", "-b", "main"],
|
|
476
|
+
capture_output=True, timeout=30, check=True)
|
|
477
|
+
_sp.run(["git", "-C", str(tmp), "config", "user.email", "t@t"],
|
|
478
|
+
capture_output=True, timeout=30, check=True)
|
|
479
|
+
_sp.run(["git", "-C", str(tmp), "config", "user.name", "t"],
|
|
480
|
+
capture_output=True, timeout=30, check=True)
|
|
481
|
+
(tmp / "a.txt").write_text("x")
|
|
482
|
+
_sp.run(["git", "-C", str(tmp), "add", "."],
|
|
483
|
+
capture_output=True, timeout=30, check=True)
|
|
484
|
+
|
|
485
|
+
async def _edit(name, params):
|
|
486
|
+
assert name == "git_commit", name
|
|
487
|
+
return "edit:pesan edit e2e"
|
|
488
|
+
fake5 = FakeLLM([StreamDone("", [_TCR(
|
|
489
|
+
"c1", "git_commit",
|
|
490
|
+
{"workdir": str(tmp), "message": "pesan awal"})]),
|
|
491
|
+
StreamDone("done", [])])
|
|
492
|
+
ctx = ConversationContext()
|
|
493
|
+
events = await _drain(run_agent("commit", ctx, cfg, llm_client=fake5,
|
|
494
|
+
confirm=_edit))
|
|
495
|
+
assert any(isinstance(e, AgentToolDone) and e.success for e in events)
|
|
496
|
+
log = _sp.run(["git", "-C", str(tmp), "log", "--format=%s"],
|
|
497
|
+
capture_output=True, text=True, timeout=30)
|
|
498
|
+
assert "pesan edit e2e" in log.stdout, log.stdout
|
|
499
|
+
|
|
500
|
+
# 12. #4 branch baru: push main ditolak → "branch" → buat + push ulang
|
|
501
|
+
async def _branch(name, params):
|
|
502
|
+
assert name == "git_push", name
|
|
503
|
+
return "branch"
|
|
504
|
+
fake6 = FakeLLM([StreamDone("", [_TCR(
|
|
505
|
+
"c1", "git_push", {"workdir": str(tmp)})]),
|
|
506
|
+
StreamDone("done", [])])
|
|
507
|
+
ctx = ConversationContext()
|
|
508
|
+
events = await _drain(run_agent(
|
|
509
|
+
"push", ctx, cfg, llm_client=fake6, confirm=_branch,
|
|
510
|
+
ask_user=lambda q: _asyncio.sleep(0, result="fitur-e2e")))
|
|
511
|
+
br = _sp.run(["git", "-C", str(tmp), "branch", "--list"],
|
|
512
|
+
capture_output=True, text=True, timeout=30)
|
|
513
|
+
assert "fitur-e2e" in br.stdout, br.stdout
|
|
514
|
+
push_done = [e for e in events if isinstance(e, AgentToolDone)]
|
|
515
|
+
assert push_done and "fitur-e2e" in str(
|
|
516
|
+
ctx.get_messages()), "push harus diarahkan ke branch baru"
|
|
517
|
+
|
|
518
|
+
print("✅ agent_loop self-test OK (12 skenario)")
|
|
406
519
|
|
|
407
520
|
async def _no_confirm(name, params):
|
|
408
521
|
raise AssertionError(f"tidak boleh minta konfirmasi untuk {name}")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Clipboard helper — salin teks ke clipboard sistem (buat /copy).
|
|
2
|
+
|
|
3
|
+
Rantai provider (yang pertama berhasil menang):
|
|
4
|
+
Termux (termux-clipboard-set) → wl-copy → xclip → xsel →
|
|
5
|
+
pbcopy (macOS) → clip/powershell (Windows).
|
|
6
|
+
|
|
7
|
+
Tak ada yang bisa (server headless / Termux:API belum install) →
|
|
8
|
+
fallback jujur: tulis ke ~/.multacd/clipboard.txt + kasih tau path-nya.
|
|
9
|
+
Tak pernah raise — return pesan buat ditampilkan ke user.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _home() -> Path:
|
|
21
|
+
return Path(os.environ.get("MULTACD_HOME", str(Path.home())))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run(cmd: list[str], text: str) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
proc = subprocess.run(cmd, input=text, capture_output=True,
|
|
27
|
+
text=True, timeout=10)
|
|
28
|
+
except (OSError, subprocess.SubprocessError):
|
|
29
|
+
return False
|
|
30
|
+
return proc.returncode == 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def copy_text(text: str) -> str:
|
|
34
|
+
"""Salin `text`. Return pesan hasil (Indonesia, siap tampil di chat)."""
|
|
35
|
+
if not text.strip():
|
|
36
|
+
return "(tidak ada teks buat disalin)"
|
|
37
|
+
data = text if len(text) <= 200_000 else text[:200_000] + "\n…(dipotong)"
|
|
38
|
+
providers: list[tuple[str, list[str]]] = []
|
|
39
|
+
if "TERMUX_VERSION" in os.environ:
|
|
40
|
+
providers.append(("termux", ["termux-clipboard-set"]))
|
|
41
|
+
providers += [
|
|
42
|
+
("wl-copy", ["wl-copy"]),
|
|
43
|
+
("xclip", ["xclip", "-selection", "clipboard"]),
|
|
44
|
+
("xsel", ["xsel", "--clipboard", "--input"]),
|
|
45
|
+
("pbcopy", ["pbcopy"]),
|
|
46
|
+
]
|
|
47
|
+
if os.name == "nt":
|
|
48
|
+
providers.append(("Windows", ["clip"]))
|
|
49
|
+
for label, cmd in providers:
|
|
50
|
+
if shutil.which(cmd[0]) and _run(cmd, data):
|
|
51
|
+
return f"tersalin ke clipboard ({label})."
|
|
52
|
+
# Fallback: file (buka + salin manual dari sana).
|
|
53
|
+
try:
|
|
54
|
+
p = _home() / ".multacd" / "clipboard.txt"
|
|
55
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
p.write_text(data, encoding="utf-8")
|
|
57
|
+
return (f"clipboard sistem tak terjangkau — teks disimpan ke {p} "
|
|
58
|
+
"(buka file itu lalu salin manual).")
|
|
59
|
+
except OSError as e:
|
|
60
|
+
return f"gagal salin ({e})."
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
import tempfile
|
|
65
|
+
|
|
66
|
+
tmp = tempfile.mkdtemp(prefix="multacd-clip-")
|
|
67
|
+
os.environ["MULTACD_HOME"] = tmp
|
|
68
|
+
# Isolasikan PATH (tanpa provider clipboard) → jatuh ke file fallback.
|
|
69
|
+
os.environ["PATH"] = tmp
|
|
70
|
+
msg = copy_text("halo clipboard")
|
|
71
|
+
assert "clipboard.txt" in msg, msg
|
|
72
|
+
assert Path(tmp, ".multacd", "clipboard.txt").read_text() == "halo clipboard"
|
|
73
|
+
assert "tidak ada teks" in copy_text(" ")
|
|
74
|
+
print("✅ clipboard self-test OK (fallback file)")
|
|
@@ -63,9 +63,8 @@ ask_before_bash: true # bash → konfirmasi dulu
|
|
|
63
63
|
ask_before_web: true # web_fetch → konfirmasi dulu
|
|
64
64
|
|
|
65
65
|
# ── Display Settings ─────────────────────────────────
|
|
66
|
-
theme: "dark" # dark | light
|
|
67
|
-
#
|
|
68
|
-
# (dark/light = alias mocha/latte)
|
|
66
|
+
theme: "dark" # dark | light (= multacd-dark/light)
|
|
67
|
+
# + multacd-min (Termux hemat) | catppuccin-* (kompat v1)
|
|
69
68
|
show_tool_calls: true # tampilkan nama tool yang dijalankan
|
|
70
69
|
show_thinking: false # tampilkan reasoning LLM (verbose mode)
|
|
71
70
|
icon_style: "auto" # auto | nerdfonts | unicode | ascii
|
|
@@ -161,7 +160,8 @@ class Config(BaseModel):
|
|
|
161
160
|
ask_before_web: bool = True
|
|
162
161
|
|
|
163
162
|
# ── Display settings ──
|
|
164
|
-
theme: Literal["dark", "light", "
|
|
163
|
+
theme: Literal["dark", "light", "multacd-dark", "multacd-light",
|
|
164
|
+
"multacd-min", "catppuccin-mocha", "catppuccin-latte",
|
|
165
165
|
"catppuccin-frappe", "catppuccin-macchiato"] = "dark"
|
|
166
166
|
show_tool_calls: bool = True
|
|
167
167
|
show_thinking: bool = False
|
|
@@ -235,7 +235,7 @@ def resolve_config_path(explicit: Path | str | None = None) -> Path:
|
|
|
235
235
|
|
|
236
236
|
def _setup_message(path: Path) -> str:
|
|
237
237
|
return (
|
|
238
|
-
"
|
|
238
|
+
"multacd — config belum ditemukan.\n"
|
|
239
239
|
"\n"
|
|
240
240
|
f"File yang dicari: {path}\n"
|
|
241
241
|
"\n"
|
|
@@ -259,7 +259,7 @@ def load_config(explicit_path: Path | str | None = None) -> Config:
|
|
|
259
259
|
try:
|
|
260
260
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
261
261
|
except OSError as e:
|
|
262
|
-
print(f"
|
|
262
|
+
print(f"Gagal membuat folder config {path.parent}: {e}", file=sys.stderr)
|
|
263
263
|
raise SystemExit(1) from e
|
|
264
264
|
|
|
265
265
|
if not path.is_file():
|
|
@@ -269,15 +269,15 @@ def load_config(explicit_path: Path | str | None = None) -> Config:
|
|
|
269
269
|
try:
|
|
270
270
|
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
271
271
|
except yaml.YAMLError as e:
|
|
272
|
-
print(f"
|
|
272
|
+
print(f"config.yaml bukan YAML valid: {path}\n Detail: {e}", file=sys.stderr)
|
|
273
273
|
raise SystemExit(1) from e
|
|
274
274
|
except OSError as e:
|
|
275
|
-
print(f"
|
|
275
|
+
print(f"Gagal membaca config {path}: {e}", file=sys.stderr)
|
|
276
276
|
raise SystemExit(1) from e
|
|
277
277
|
|
|
278
278
|
if not isinstance(raw, dict) or not raw:
|
|
279
279
|
print(
|
|
280
|
-
f"
|
|
280
|
+
f"config.yaml kosong atau tidak berbentuk key: value: {path}\n"
|
|
281
281
|
" Isi dengan contoh config (jalankan tanpa config untuk melihatnya).",
|
|
282
282
|
file=sys.stderr,
|
|
283
283
|
)
|
|
@@ -286,7 +286,7 @@ def load_config(explicit_path: Path | str | None = None) -> Config:
|
|
|
286
286
|
try:
|
|
287
287
|
cfg = Config(**raw)
|
|
288
288
|
except ValidationError as e:
|
|
289
|
-
print(f"
|
|
289
|
+
print(f"config.yaml tidak valid: {path}", file=sys.stderr)
|
|
290
290
|
for err in e.errors():
|
|
291
291
|
field = ".".join(str(p) for p in err["loc"])
|
|
292
292
|
print(f" • {field}: {err['msg']}", file=sys.stderr)
|
|
@@ -294,10 +294,10 @@ def load_config(explicit_path: Path | str | None = None) -> Config:
|
|
|
294
294
|
raise SystemExit(1) from e
|
|
295
295
|
|
|
296
296
|
if not cfg.model.strip():
|
|
297
|
-
print("
|
|
297
|
+
print("Field `model` wajib diisi (contoh: anthropic/claude-sonnet-4-6).", file=sys.stderr)
|
|
298
298
|
raise SystemExit(1)
|
|
299
299
|
if not cfg.api_key.strip():
|
|
300
|
-
print("
|
|
300
|
+
print("Field `api_key` wajib diisi (BYOK — pakai key provider kamu).", file=sys.stderr)
|
|
301
301
|
raise SystemExit(1)
|
|
302
302
|
|
|
303
303
|
return cfg
|