dulus 3.14.18__tar.gz → 3.15.0__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.
- {dulus-3.14.18/dulus.egg-info → dulus-3.15.0}/PKG-INFO +3 -1
- {dulus-3.14.18 → dulus-3.15.0}/README.md +2 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/CHANGELOG.md +1 -1
- {dulus-3.14.18 → dulus-3.15.0}/docs/news.md +5 -1
- {dulus-3.14.18 → dulus-3.15.0/dulus.egg-info}/PKG-INFO +3 -1
- {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/SOURCES.txt +2 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/top_level.txt +1 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus.py +110 -3
- {dulus-3.14.18 → dulus-3.15.0}/dulus_account.py +67 -10
- {dulus-3.14.18 → dulus-3.15.0}/providers.py +11 -3
- {dulus-3.14.18 → dulus-3.15.0}/pyproject.toml +1 -1
- dulus-3.15.0/tests/test_empty_assistant_turn.py +38 -0
- dulus-3.15.0/tests/test_login_gate.py +278 -0
- {dulus-3.14.18 → dulus-3.15.0}/LICENSE +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/MANIFEST.in +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/agent.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/algolia_search.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/analytics.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/agents_bridge.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/compressor.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/context.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/githook.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/marketplace.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/mempalace_bridge.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/paths.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/personas.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/plugins.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/server.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/session_utils.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/tasks.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/theme_pack.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/backend/themes.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/banners/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/banners/banner_rotator.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/banners/banners_dulus_ai.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/batch_api.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/checkpoint/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/checkpoint/hooks.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/checkpoint/store.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/checkpoint/types.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/__main__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/ansi.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/assets/ascii/dulus_logo.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/assets/ascii/eagle.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/assets/ascii/terminal_art.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/banners.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/demo.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/effects.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/effort_slider.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/progress.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/py.typed +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/spinners.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cli_animations/status.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/clipboard_utils.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/cloudsave.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/common.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/compaction.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/config.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/context.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/plugin.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/display_blocks.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/API.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/BRAND_GUIDE.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/CONTRIBUTING.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/DEPLOYMENT.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/FAQ.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/GETTING_STARTED.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/SECURITY.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/architecture.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/dashboard/index.html +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/divider.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/dulus-bird.png +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/generate.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/nvidia-models.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/particle-playground.html +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/personas/index.html +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/preview.html +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/python-console.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/dulus-os.png +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/hero.png +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/pyc_heap.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/pyc_receipts.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/pyc_trap.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/task-board.png +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/readme/webchat.png +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-agents.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-brainstorm.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-bridges.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-features.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-freetier.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-memory.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-models.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-perms.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-plugins.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-quickstart.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/sec-ssj.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/spinners.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/split-pane.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/terminal-boot.svg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/docs/termux-edge.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus.egg-info/requires.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_bar_client.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/client.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/commands.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/config.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/hub.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_mcp/types.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_quick_menu.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/add_dir_manager.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/afk_mode.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/approval_runtime.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/background_tasks.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/clipboard_utils.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/diff_visualizer.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/export_import.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/hook_engine.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/isolate.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/notification_manager.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/session_fork.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/shell_mode.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/todo_visualizer.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/tree_ls.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/wire_events.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/dulus_tools/yolo_mode.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/file_filter.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/fs_compass.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/governance.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/input.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/license_manager.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/audit.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/consolidator.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/context.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/mempalace_bridge.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/offload.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/palace.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/scan.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/askuserquestion_after_numbered_list.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/background_jobs_no_extra_timers.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/cleverness_over_resources.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/corporate_tls_breaks_curl_cffi.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/hardened_rules.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/investigator_workflow_heuristic.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/native_code_over_sketchy_plugins.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/no_personal_info_in_code.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/searches_include_modified_date.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/tmux_send_keys_power_patterns.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/token_optimization_technique.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/tool_chaining_capability.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/insights/webbridge_evaluate_first_pattern.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/seeds/short_memory.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/sessions.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/store.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/types.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/memory/vector_search.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/model_params.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/multi_agent/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/multi_agent/subagent.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/multi_agent/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/paste_placeholders.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/personality_config.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/plugin/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/plugin/loader.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/plugin/recommend.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/plugin/store.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/plugin/types.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/profiles.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/pyconsole.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/setup.cfg +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/builtin.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/LICENSE-obsidian-skills +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/README.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/defuddle/SKILL.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/json-canvas/SKILL.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/json-canvas/references/EXAMPLES.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-bases/SKILL.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-bases/references/FUNCTIONS_REFERENCE.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-cli/SKILL.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/SKILL.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/references/CALLOUTS.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/references/EMBEDS.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/obsidian-markdown/references/PROPERTIES.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/README.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/SKILL.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/addresses.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/advancedbackgroundchecks.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/beenverified.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/clustal.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/clustrmaps.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/cyberbackgroundchecks.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/familytreenow.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/fastpeoplesearch.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/intelius.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/mylife.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/nuwber.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/peekyou.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/peoplefinders.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/radaris.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/rehold.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/searchpeoplefree.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/socialcatfish.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/spokeo.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/thatsthem.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/truepeoplesearch.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/usphonebook.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/brokers/whitepages.json +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/legal/ccpa.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/legal/drop.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/legal/gdpr.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/methods.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/site-playbooks.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/references/state-machine.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/autopilot.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/badbool.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/brokers.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/cdp.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/config.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/crypto.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/dossier.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/email_modes.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/emailer.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/ledger.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/legal.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/paths.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/pdd.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/registry.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/report.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/scan.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/storage.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/tiers.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/scripts/vectors.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/consent/authorization.md +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/ccpa-authorized-agent.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/ccpa-deletion.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/ccpa-indirect-deletion.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/gdpr-erasure.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/bundled/unbroker/templates/emails/generic-optout.txt +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/clawhub.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/executor.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/loader.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skill/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/skills.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/soul.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/spinner.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/steer_input.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/subagent.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/task/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/task/store.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/task/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/task/types.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_afk_yolo.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_agent_tool_inputs.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_approval_runtime.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_background_task_tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_background_tasks.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_chatgpt_oauth.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_checkpoint.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_cli_animations_integration.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_clipboard_utils.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_compaction.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_compaction_failures.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_composio_tool_generator.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_diff_view.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_diff_visualization.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_display_blocks.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_dulus_account.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_export_import.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_grok_usage.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_hook_engine.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_injection_fix.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_interruptible_stream.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_json_protocol.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_license.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_mcp.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_mcp_algolia.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_memory.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_memory_home_resolution.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_notification_manager.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_persistence_safety.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_plugin.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_profile_isolation.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_profile_plugin_scope.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_provider_errors.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_provider_stability.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_public_surface.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_repl_resilience.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_session_fork.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_shell_detection.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_shell_mode.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_skills.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_skillssh_algolia.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_steer_input.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_subagent.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_task.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_telegram_buffer.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_think_tool.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_todo_tool.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_todo_visualization.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_tool_registry.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_tool_safety.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_tree_ls.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_voice.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_webbridge_tabs.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tests/test_wire_events.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tmux_tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/token_budget.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tool_registry.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tools_background.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tools_think.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/tools_todo.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/ui/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/ui/input.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/ui/render.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/updater.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/audio_utils.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/keyterms.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/recorder.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/stt.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/tts.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/voice/wake_word.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webbridge/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webbridge/core.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webbridge/tools.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webchat.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webchat_server.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/__init__.py +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/app.js +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/index.html +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/webchat_ui/style.css +0 -0
- {dulus-3.14.18 → dulus-3.15.0}/welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.15.0
|
|
4
4
|
Summary: Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS.
|
|
5
5
|
Author-email: KevRojo <dev@dulus.ai>
|
|
6
6
|
Maintainer-email: KevRojo <dev@dulus.ai>
|
|
@@ -464,6 +464,8 @@ Those numbers are not a vanity dashboard. They explain the product: Dulus is bui
|
|
|
464
464
|
|
|
465
465
|
## Install in 30 seconds
|
|
466
466
|
|
|
467
|
+
> **Dulus runs on a free Dulus account.** The first `dulus` opens your browser to sign in; after that the session is remembered. CI, scripts and servers: set `DULUS_API_KEY` to a `dulus_sk_*` key from `/login dulus key`. `--version` and `--help` never ask.
|
|
468
|
+
|
|
467
469
|
If Python 3.11 or newer is already installed:
|
|
468
470
|
|
|
469
471
|
```bash
|
|
@@ -359,6 +359,8 @@ Those numbers are not a vanity dashboard. They explain the product: Dulus is bui
|
|
|
359
359
|
|
|
360
360
|
## Install in 30 seconds
|
|
361
361
|
|
|
362
|
+
> **Dulus runs on a free Dulus account.** The first `dulus` opens your browser to sign in; after that the session is remembered. CI, scripts and servers: set `DULUS_API_KEY` to a `dulus_sk_*` key from `/login dulus key`. `--version` and `--help` never ask.
|
|
363
|
+
|
|
362
364
|
If Python 3.11 or newer is already installed:
|
|
363
365
|
|
|
364
366
|
```bash
|
|
@@ -88,7 +88,7 @@ set a provider key, or `/model ollama/<name>` to pick a supported route.
|
|
|
88
88
|
- **First-turn amnesia**: agent + lookback strip GUI/REPL gold/soul/welcome *display* blobs before the provider call, so web-history consolidate no longer treats gold as “last assistant reply” and drops real context
|
|
89
89
|
- **MemPalace dual-path noise**: per-turn inject (CLI + WebChat) skips `short_memory` / soul — already in the system prompt
|
|
90
90
|
- **Second `/compact` archive wipe**: `save_loopback_archive` refuses to shrink; Loopback keeps the full pre-compact history across N compacts
|
|
91
|
-
- **WebChat gold preload**: session seeds gold *display* copies for the transcript; model source of truth is still the system prompt (public GUI stays as-is — this is server/engine, not
|
|
91
|
+
- **WebChat gold preload**: session seeds gold *display* copies for the transcript; model source of truth is still the system prompt (public GUI stays as-is — this is server/engine, not a port of the private GUI)
|
|
92
92
|
|
|
93
93
|
### Tests
|
|
94
94
|
- `test_gold_memory_preload` + `test_lookback_compact` (11 passed)
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## 🔥🔥🔥 News (Pacific Time)
|
|
4
4
|
|
|
5
|
-
- November
|
|
5
|
+
- November 9, 2026 (**🔐 v3.15.0 — Dulus now runs on a Dulus account**): **Sign in once, then it's all yours.** From this release on, the first launch asks you to sign in with a free Dulus account — your browser opens, you approve, and you're back in the terminal. That's the whole ritual: the session is remembered and renewed quietly, and when Dulus can't be reached for a renewal you keep working offline. CI, scripts and servers skip the browser: set `DULUS_API_KEY` to a `dulus_sk_*` key minted with `/login dulus key`. `--version` and `--help` stay open to everyone, and your own provider keys (OpenAI, Anthropic, Ollama…) work exactly as before — they just ride on a signed-in Dulus now. `pip install -U dulus`. 🦅🇩🇴
|
|
6
|
+
|
|
7
|
+
- November 3, 2026 (**📡 v3.14.19 — the boot ident signs with our own name**): **One small, public-facing fix.** The Dulus Radio station ident that prints on every interactive boot now signs with the product's own name. Same board, same dispatch, same sign-off about this repo being a mirror — only the signature changed. `pip install -U dulus`. 🦅🇩🇴
|
|
8
|
+
|
|
9
|
+
- November 3, 2026 (**📡 v3.14.18 — Dulus Radio gets a station ident**): **The first screen you see now says who is talking.** Dulus Radio — the dispatch board that prints on every interactive boot — gained a station ident: *Dulus · Santo Domingo 🇩🇴 · what shipped while you were away*, so a fresh install learns who builds this and what the board is for instead of reading a bare "ON AIR". The sign-off states the honest bit too: **this repo is a mirror, and the signed desktop build runs ahead of it.** **Long dispatch headlines no longer wreck the layout** — a headline is capped at 68 characters and the overflow moves into the body, so the board stays readable on an 80-column terminal and no detail is lost; the cut prefers a sentence or clause boundary over splitting a word. The README also now documents the real distance between this mirror and the signed line: **2.2× the commits, 4.3× the code, 13.3× the 30-day velocity.** `pip install -U dulus`. 🦅🇩🇴
|
|
6
10
|
|
|
7
11
|
- October 29, 2026 (**🧠 v6.4.1 — memory that stays with you**): **Memory belongs to the profile now.** Recall was missing things it had already saved; both sides of the memory path are scoped to the profile you are actually running, so what you save is what comes back. **Context limits finally add up** — the per-source budgets are one real ceiling instead of separate numbers that never summed. **Bash commands no longer cut themselves off mid-run**, so a long build returns its whole output instead of a truncated tail. **Native installers are back on both platforms**: a signed `.exe` for Windows and a `.dmg` for macOS ARM64, no Python setup required. A **silent nudge in the taskbar** tells you a reply is waiting instead of leaving you staring at a dead window. And **Dulus Business** — the deployable stack and the owners' dashboard — is back in the repo. Grab the signed build from [dulus-updates](https://github.com/Dulus-Ai/dulus-updates/releases/tag/v6.4.1), or `pip install -U dulus` for the open runtime. 🦅🇩🇴
|
|
8
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.15.0
|
|
4
4
|
Summary: Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS.
|
|
5
5
|
Author-email: KevRojo <dev@dulus.ai>
|
|
6
6
|
Maintainer-email: KevRojo <dev@dulus.ai>
|
|
@@ -464,6 +464,8 @@ Those numbers are not a vanity dashboard. They explain the product: Dulus is bui
|
|
|
464
464
|
|
|
465
465
|
## Install in 30 seconds
|
|
466
466
|
|
|
467
|
+
> **Dulus runs on a free Dulus account.** The first `dulus` opens your browser to sign in; after that the session is remembered. CI, scripts and servers: set `DULUS_API_KEY` to a `dulus_sk_*` key from `/login dulus key`. `--version` and `--help` never ask.
|
|
468
|
+
|
|
467
469
|
If Python 3.11 or newer is already installed:
|
|
468
470
|
|
|
469
471
|
```bash
|
|
@@ -288,6 +288,7 @@ tests/test_diff_view.py
|
|
|
288
288
|
tests/test_diff_visualization.py
|
|
289
289
|
tests/test_display_blocks.py
|
|
290
290
|
tests/test_dulus_account.py
|
|
291
|
+
tests/test_empty_assistant_turn.py
|
|
291
292
|
tests/test_export_import.py
|
|
292
293
|
tests/test_grok_usage.py
|
|
293
294
|
tests/test_hook_engine.py
|
|
@@ -295,6 +296,7 @@ tests/test_injection_fix.py
|
|
|
295
296
|
tests/test_interruptible_stream.py
|
|
296
297
|
tests/test_json_protocol.py
|
|
297
298
|
tests/test_license.py
|
|
299
|
+
tests/test_login_gate.py
|
|
298
300
|
tests/test_mcp.py
|
|
299
301
|
tests/test_mcp_algolia.py
|
|
300
302
|
tests/test_memory.py
|
|
@@ -375,7 +375,7 @@ try:
|
|
|
375
375
|
from importlib.metadata import version as _pkg_version
|
|
376
376
|
VERSION = _pkg_version("dulus")
|
|
377
377
|
except Exception:
|
|
378
|
-
VERSION = "3.
|
|
378
|
+
VERSION = "3.15.0" # dev fallback — keep in sync with pyproject.toml
|
|
379
379
|
|
|
380
380
|
# ── Machine-readable protocol output (`--output json`) ─────────────────────
|
|
381
381
|
# Dulus is increasingly run as a child process by another agent runtime (an
|
|
@@ -9042,7 +9042,7 @@ def _print_news_broadcast(
|
|
|
9042
9042
|
# Station ident. This is the first thing a new install ever reads, so it
|
|
9043
9043
|
# says who is broadcasting and what the board is actually for.
|
|
9044
9044
|
print(
|
|
9045
|
-
clr("
|
|
9045
|
+
clr(" Dulus", "white", "bold")
|
|
9046
9046
|
+ clr(" · Santo Domingo 🇩🇴 · ", "dim")
|
|
9047
9047
|
+ clr("what shipped while you were away", "cyan", "dim")
|
|
9048
9048
|
)
|
|
@@ -11241,7 +11241,7 @@ def repl(config: dict, initial_prompt: str | None = None):
|
|
|
11241
11241
|
"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⢾⣯⣧⣬⣭⣤⡶⠖⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
|
|
11242
11242
|
]
|
|
11243
11243
|
_DULUS_LOGO.append(" " + clr("v" + VERSION, "green", "bold"))
|
|
11244
|
-
_DULUS_LOGO.append(" " + clr("📡 Dulus Radio ·
|
|
11244
|
+
_DULUS_LOGO.append(" " + clr("📡 Dulus Radio · ON AIR at boot", "cyan", "dim"))
|
|
11245
11245
|
_DULUS_LOGO.append(" ")
|
|
11246
11246
|
|
|
11247
11247
|
# Shared Dulus animation language for the short boot beat. Redirected
|
|
@@ -13075,6 +13075,110 @@ def repl(config: dict, initial_prompt: str | None = None):
|
|
|
13075
13075
|
|
|
13076
13076
|
# ── Entry point ────────────────────────────────────────────────────────────
|
|
13077
13077
|
|
|
13078
|
+
def _attached_to_msys_pty(stream) -> bool:
|
|
13079
|
+
"""Is this stream a mintty terminal (Git Bash, MSYS2, Cygwin)?
|
|
13080
|
+
|
|
13081
|
+
mintty hands Windows programs a named pipe instead of a console, so
|
|
13082
|
+
isatty() says nobody is there even with a person typing. The pipe's name
|
|
13083
|
+
gives the terminal away, and ordinary pipes (`| tail`, a subprocess
|
|
13084
|
+
capture) never carry it. MSYSTEM can't be trusted for this: every child
|
|
13085
|
+
of such a shell inherits it, scripts and scheduled jobs included.
|
|
13086
|
+
"""
|
|
13087
|
+
if sys.platform != "win32":
|
|
13088
|
+
return False
|
|
13089
|
+
try:
|
|
13090
|
+
import ctypes
|
|
13091
|
+
import msvcrt
|
|
13092
|
+
from ctypes import wintypes
|
|
13093
|
+
|
|
13094
|
+
class _FileNameInfo(ctypes.Structure):
|
|
13095
|
+
_fields_ = [("FileNameLength", wintypes.DWORD), ("FileName", wintypes.WCHAR * 260)]
|
|
13096
|
+
|
|
13097
|
+
info = _FileNameInfo()
|
|
13098
|
+
handle = wintypes.HANDLE(msvcrt.get_osfhandle(stream.fileno()))
|
|
13099
|
+
file_name_info = 2 # FILE_INFO_BY_HANDLE_CLASS.FileNameInfo
|
|
13100
|
+
if not ctypes.windll.kernel32.GetFileInformationByHandleEx(
|
|
13101
|
+
handle, file_name_info, ctypes.byref(info), ctypes.sizeof(info)):
|
|
13102
|
+
return False
|
|
13103
|
+
name = info.FileName[: info.FileNameLength // 2]
|
|
13104
|
+
except Exception:
|
|
13105
|
+
return False
|
|
13106
|
+
return re.match(r"\\(cygwin|msys)-[0-9a-fA-F]+-pty\d+-(from|to)-master", name) is not None
|
|
13107
|
+
|
|
13108
|
+
|
|
13109
|
+
def _login_gate_can_prompt() -> bool:
|
|
13110
|
+
"""Is a person there to see a sign-in link? CI never counts."""
|
|
13111
|
+
if os.environ.get("CI"):
|
|
13112
|
+
return False
|
|
13113
|
+
for stream in (sys.stdout, sys.stderr):
|
|
13114
|
+
if stream is None:
|
|
13115
|
+
continue
|
|
13116
|
+
try:
|
|
13117
|
+
if stream.isatty() or _attached_to_msys_pty(stream):
|
|
13118
|
+
return True
|
|
13119
|
+
except Exception:
|
|
13120
|
+
pass
|
|
13121
|
+
return False
|
|
13122
|
+
|
|
13123
|
+
|
|
13124
|
+
def _require_dulus_login(args) -> None:
|
|
13125
|
+
"""Dulus runs on a free Dulus account: no account, no Dulus.
|
|
13126
|
+
|
|
13127
|
+
Checked once per launch, after --version/--help (those stay open to
|
|
13128
|
+
everyone). Background job runners are spawned by a session that already
|
|
13129
|
+
passed here, and `dulus -c "login dulus"` / `-c logout` stay reachable so
|
|
13130
|
+
a broken sign-in can always be fixed from the command line. Renewal only
|
|
13131
|
+
happens when a person launched Dulus; background children and `-c` runs
|
|
13132
|
+
take the stored session at its word so parallel launches never race to
|
|
13133
|
+
rotate the same refresh token.
|
|
13134
|
+
"""
|
|
13135
|
+
if args.run_tool or args.job_id or args.job_path:
|
|
13136
|
+
return
|
|
13137
|
+
if args.exec_cmd:
|
|
13138
|
+
verb = " ".join(args.exec_cmd).strip().strip("\"'").lstrip("/").split(" ", 1)[0].lower()
|
|
13139
|
+
if verb in ("login", "logout"):
|
|
13140
|
+
return
|
|
13141
|
+
|
|
13142
|
+
import dulus_account
|
|
13143
|
+
|
|
13144
|
+
protocol = _protocol_enabled()
|
|
13145
|
+
can_prompt = not protocol and _login_gate_can_prompt()
|
|
13146
|
+
status = dulus_account.session_status(renew=can_prompt and not args.exec_cmd)
|
|
13147
|
+
if status == "active":
|
|
13148
|
+
return
|
|
13149
|
+
if status == "offline":
|
|
13150
|
+
print(clr("[dulus] Couldn't reach Dulus to renew your sign-in — carrying on offline.", "gray"),
|
|
13151
|
+
file=sys.stderr)
|
|
13152
|
+
return
|
|
13153
|
+
|
|
13154
|
+
if status == "invalid_key":
|
|
13155
|
+
message = ("DULUS_API_KEY is set but isn't a Dulus key (those start with dulus_sk_). Replace "
|
|
13156
|
+
"it with one minted by `/login dulus key`, or unset it to use your sign-in.")
|
|
13157
|
+
elif not can_prompt:
|
|
13158
|
+
message = ("Dulus needs a free Dulus account. Sign in once with `dulus -c \"login dulus\"` "
|
|
13159
|
+
"(or just run `dulus` in a terminal). For CI, scripts and servers, set "
|
|
13160
|
+
"DULUS_API_KEY to a dulus_sk_* key minted with `/login dulus key`.")
|
|
13161
|
+
else:
|
|
13162
|
+
message = ""
|
|
13163
|
+
if message:
|
|
13164
|
+
if protocol:
|
|
13165
|
+
_emit_error(message)
|
|
13166
|
+
else:
|
|
13167
|
+
print(f"[dulus] {message}", file=sys.stderr)
|
|
13168
|
+
sys.exit(2)
|
|
13169
|
+
|
|
13170
|
+
print(clr("\n Dulus runs on a free Dulus account — sign in once and you're in.\n", "cyan"))
|
|
13171
|
+
try:
|
|
13172
|
+
token = dulus_account.login(notify=lambda m: print(clr(m, "cyan")))
|
|
13173
|
+
except KeyboardInterrupt:
|
|
13174
|
+
token = None
|
|
13175
|
+
if not token:
|
|
13176
|
+
print(clr("[dulus] Sign-in didn't finish. Run `dulus` again whenever you're ready.", "yellow"),
|
|
13177
|
+
file=sys.stderr)
|
|
13178
|
+
sys.exit(1)
|
|
13179
|
+
ok("Signed in — welcome aboard. 🦅")
|
|
13180
|
+
|
|
13181
|
+
|
|
13078
13182
|
def main():
|
|
13079
13183
|
parser = argparse.ArgumentParser(
|
|
13080
13184
|
prog="dulus",
|
|
@@ -13139,6 +13243,9 @@ def main():
|
|
|
13139
13243
|
from config import load_config, save_config, has_api_key
|
|
13140
13244
|
from providers import detect_provider, PROVIDERS
|
|
13141
13245
|
|
|
13246
|
+
# No account, no Dulus: sign in (or learn how) before anything else runs.
|
|
13247
|
+
_require_dulus_login(args)
|
|
13248
|
+
|
|
13142
13249
|
# ── First-run welcome wizard ──────────────────────────────────────────
|
|
13143
13250
|
# Runs BEFORE load_config() so we detect the absence of config.json.
|
|
13144
13251
|
# The wizard mutates config in-place and persists it; subsequent
|
|
@@ -43,6 +43,7 @@ _AUTH0_SCOPES = "openid profile email"
|
|
|
43
43
|
# Refresh slightly early so a request never leaves with a token that expires
|
|
44
44
|
# while it is in flight.
|
|
45
45
|
_EXPIRY_BUFFER_SECONDS = 60
|
|
46
|
+
API_KEY_PREFIX = "dulus_sk_" # what `/login dulus key` mints
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def _is_text_browser(name: str) -> bool:
|
|
@@ -157,12 +158,13 @@ def _persist_tokens(payload: dict) -> dict:
|
|
|
157
158
|
return store
|
|
158
159
|
|
|
159
160
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
161
|
+
def _renew(refresh_token: str, timeout: float = 30) -> tuple[str, dict | None]:
|
|
162
|
+
"""One refresh round-trip against ``/v1/oauth/token``.
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
Returns ``("ok", store)`` once the new tokens are saved, ``("refused", None)``
|
|
165
|
+
when Dulus said no (revoked, expired for good, account gone) and
|
|
166
|
+
``("unreachable", None)`` when it could not answer — no network, a timeout,
|
|
167
|
+
or a 5xx/429 on our side.
|
|
166
168
|
"""
|
|
167
169
|
import requests
|
|
168
170
|
|
|
@@ -174,13 +176,37 @@ def refresh(refresh_token: str) -> dict | None:
|
|
|
174
176
|
"client_id": DULUS_OAUTH_CLIENT_ID,
|
|
175
177
|
"refresh_token": refresh_token,
|
|
176
178
|
},
|
|
177
|
-
timeout=
|
|
179
|
+
timeout=timeout,
|
|
178
180
|
)
|
|
179
|
-
if response.status_code != 200:
|
|
180
|
-
return None
|
|
181
|
-
return _persist_tokens(response.json())
|
|
182
181
|
except Exception:
|
|
183
|
-
return None
|
|
182
|
+
return "unreachable", None
|
|
183
|
+
if response.status_code in (400, 401, 403):
|
|
184
|
+
return "refused", None
|
|
185
|
+
if response.status_code != 200:
|
|
186
|
+
return "unreachable", None
|
|
187
|
+
try:
|
|
188
|
+
payload = dict(response.json())
|
|
189
|
+
except Exception:
|
|
190
|
+
return "unreachable", None
|
|
191
|
+
if not payload.get("access_token"):
|
|
192
|
+
return "unreachable", None
|
|
193
|
+
if not payload.get("refresh_token"):
|
|
194
|
+
# RFC 6749 §6: no new refresh token issued means the old one stays valid.
|
|
195
|
+
payload["refresh_token"] = refresh_token
|
|
196
|
+
try:
|
|
197
|
+
return "ok", _persist_tokens(payload)
|
|
198
|
+
except Exception:
|
|
199
|
+
return "unreachable", None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def refresh(refresh_token: str) -> dict | None:
|
|
203
|
+
"""Exchange a refresh token for a fresh access token.
|
|
204
|
+
|
|
205
|
+
The server rotates refresh tokens, so the response carries a new one that
|
|
206
|
+
replaces the old. A failure here means the token was revoked or replayed
|
|
207
|
+
and the user has to sign in again.
|
|
208
|
+
"""
|
|
209
|
+
return _renew(refresh_token)[1]
|
|
184
210
|
|
|
185
211
|
|
|
186
212
|
def login(notify: Callable[[str], Any] = print, force_browser: bool = False) -> str | None:
|
|
@@ -379,6 +405,37 @@ def access_token(notify: Callable[[str], Any] = print) -> str:
|
|
|
379
405
|
return ""
|
|
380
406
|
|
|
381
407
|
|
|
408
|
+
def session_status(renew: bool = True) -> str:
|
|
409
|
+
"""Is this machine signed in to a Dulus account? Never prompts.
|
|
410
|
+
|
|
411
|
+
``"active"`` a live session, or a ``dulus_sk_*`` key in DULUS_API_KEY
|
|
412
|
+
``"offline"`` a session exists but its renewal couldn't reach Dulus —
|
|
413
|
+
let the person keep working
|
|
414
|
+
``"invalid_key"`` DULUS_API_KEY is set but isn't a Dulus key
|
|
415
|
+
``"none"`` never signed in, signed out, or Dulus refused the renewal
|
|
416
|
+
|
|
417
|
+
With ``renew`` it talks to Dulus only when the access token has expired —
|
|
418
|
+
the same renewal :func:`access_token` runs on first use anyway. Without it
|
|
419
|
+
(background children, scripts) a stored refresh token is taken at its word
|
|
420
|
+
and renewed lazily, so parallel launches never race to rotate it.
|
|
421
|
+
"""
|
|
422
|
+
explicit = (os.environ.get("DULUS_API_KEY") or "").strip()
|
|
423
|
+
if explicit:
|
|
424
|
+
looks_right = explicit.startswith(API_KEY_PREFIX) and len(explicit) >= len(API_KEY_PREFIX) + 16
|
|
425
|
+
return "active" if looks_right else "invalid_key"
|
|
426
|
+
|
|
427
|
+
store = load_store()
|
|
428
|
+
if store.get("access_token") and not _token_expired(store):
|
|
429
|
+
return "active"
|
|
430
|
+
refresh_token = store.get("refresh_token")
|
|
431
|
+
if not refresh_token:
|
|
432
|
+
return "none"
|
|
433
|
+
if not renew:
|
|
434
|
+
return "active"
|
|
435
|
+
state, _ = _renew(refresh_token, timeout=10)
|
|
436
|
+
return {"ok": "active", "refused": "none"}.get(state, "offline")
|
|
437
|
+
|
|
438
|
+
|
|
382
439
|
def auth_headers(notify: Callable[[str], Any] = print) -> dict:
|
|
383
440
|
"""Authorization header for Dulus API calls, or empty when signed out."""
|
|
384
441
|
token = access_token(notify)
|
|
@@ -3801,7 +3801,11 @@ def messages_to_anthropic(messages: list) -> list:
|
|
|
3801
3801
|
"name": tc["name"],
|
|
3802
3802
|
"input": tc["input"],
|
|
3803
3803
|
})
|
|
3804
|
-
|
|
3804
|
+
# An empty assistant turn (reasoning-only stall) has no blocks, and
|
|
3805
|
+
# Anthropic rejects empty content. Skip it — the API merges the
|
|
3806
|
+
# surrounding consecutive user turns.
|
|
3807
|
+
if blocks:
|
|
3808
|
+
result.append({"role": "assistant", "content": blocks})
|
|
3805
3809
|
i += 1
|
|
3806
3810
|
|
|
3807
3811
|
elif role == "tool":
|
|
@@ -3890,11 +3894,15 @@ def messages_to_openai(messages: list, ollama_native_images: bool = False, model
|
|
|
3890
3894
|
result.append(msg_out)
|
|
3891
3895
|
|
|
3892
3896
|
elif role == "assistant":
|
|
3893
|
-
|
|
3897
|
+
tcs = m.get("tool_calls", [])
|
|
3898
|
+
# null content is only legal next to tool_calls. A reasoning-only
|
|
3899
|
+
# stall leaves an empty assistant turn in history; sending it as
|
|
3900
|
+
# null 400s every later request in the session.
|
|
3901
|
+
msg: dict = {"role": "assistant",
|
|
3902
|
+
"content": m.get("content") or (None if tcs else "")}
|
|
3894
3903
|
if "thinking" in m and m["thinking"]:
|
|
3895
3904
|
msg["reasoning_content"] = m["thinking"]
|
|
3896
3905
|
|
|
3897
|
-
tcs = m.get("tool_calls", [])
|
|
3898
3906
|
if tcs:
|
|
3899
3907
|
msg["tool_calls"] = []
|
|
3900
3908
|
for tc in tcs:
|
|
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "dulus"
|
|
13
|
-
version = "3.
|
|
13
|
+
version = "3.15.0"
|
|
14
14
|
description = "Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""A reasoning-only stall leaves an empty assistant turn in history.
|
|
2
|
+
|
|
3
|
+
agent.run keeps {"role": "assistant", "content": "", "tool_calls": []} after
|
|
4
|
+
the model spends its whole budget on reasoning. Both converters must turn it
|
|
5
|
+
into something the API accepts, or every later prompt in the session 400s.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import providers
|
|
10
|
+
|
|
11
|
+
STALL = [
|
|
12
|
+
{"role": "user", "content": "Prove the theorem."},
|
|
13
|
+
{"role": "assistant", "content": "", "thinking": "", "tool_calls": []},
|
|
14
|
+
{"role": "user", "content": "Try again, shorter."},
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_openai_empty_assistant_gets_string_content():
|
|
19
|
+
out = providers.messages_to_openai(STALL)
|
|
20
|
+
assert out[1] == {"role": "assistant", "content": ""}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_openai_tool_call_turn_keeps_null_content():
|
|
24
|
+
msgs = [
|
|
25
|
+
{"role": "user", "content": "ls"},
|
|
26
|
+
{"role": "assistant", "content": "",
|
|
27
|
+
"tool_calls": [{"id": "c1", "name": "Bash", "input": {"command": "ls"}}]},
|
|
28
|
+
{"role": "tool", "tool_call_id": "c1", "name": "Bash", "content": "a.txt"},
|
|
29
|
+
]
|
|
30
|
+
out = providers.messages_to_openai(msgs)
|
|
31
|
+
assert out[1]["content"] is None
|
|
32
|
+
assert out[1]["tool_calls"][0]["id"] == "c1"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_anthropic_empty_assistant_is_dropped():
|
|
36
|
+
out = providers.messages_to_anthropic(STALL)
|
|
37
|
+
assert all(m["content"] for m in out)
|
|
38
|
+
assert [m["role"] for m in out] == ["user", "user"]
|