dulus 0.2.50__tar.gz → 0.2.52__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-0.2.50/dulus.egg-info → dulus-0.2.52}/PKG-INFO +3 -2
- {dulus-0.2.50 → dulus-0.2.52}/README.md +2 -1
- {dulus-0.2.50 → dulus-0.2.52}/data/context.json +19 -32
- {dulus-0.2.50 → dulus-0.2.52/dulus.egg-info}/PKG-INFO +3 -2
- {dulus-0.2.50 → dulus-0.2.52}/dulus.py +15 -27
- {dulus-0.2.50 → dulus-0.2.52}/dulus_gui.py +54 -21
- {dulus-0.2.50 → dulus-0.2.52}/gui/agent_bridge.py +2 -2
- {dulus-0.2.50 → dulus-0.2.52}/gui/session_utils.py +60 -52
- {dulus-0.2.50 → dulus-0.2.52}/gui/sidebar.py +79 -53
- {dulus-0.2.50 → dulus-0.2.52}/pyproject.toml +1 -1
- {dulus-0.2.50 → dulus-0.2.52}/webchat_server.py +15 -1
- {dulus-0.2.50 → dulus-0.2.52}/LICENSE +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/MANIFEST.in +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/agent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/agents_bridge.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/compressor.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/context.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/githook.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/marketplace.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/personas.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/plugins.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/server.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/backend/tasks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/batch_api.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/checkpoint/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/checkpoint/hooks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/checkpoint/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/checkpoint/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/claude_code_watcher.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/clipboard_utils.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/cloudsave.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/common.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/compaction.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/config.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/context.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/active_persona.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/marketplace.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/personas.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/data/tasks.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/README.md +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/api.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/architecture.md +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/dashboard/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/divider.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/generate.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/hero.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/news.md +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/particle-playground.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/personas/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/poetry-banner.png +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/preview.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-agents.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-features.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-memory.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-models.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-perms.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/spinners.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/split-pane.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus_mcp/client.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus_mcp/config.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/dulus_mcp/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/chat_widget.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/main_window.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/personas.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/settings_dialog.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/tasks_view.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/themes.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/gui/tool_panel.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/input.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/license_manager.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/audit.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/consolidator.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/context.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/offload.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/palace.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/scan.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/sessions.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/memory/vector_search.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/multi_agent/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/multi_agent/subagent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/multi_agent/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/offload_helper.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/plugin/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/plugin/autoadapter.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/plugin/loader.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/plugin/recommend.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/plugin/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/plugin/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/providers.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/README.md +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/components.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/wallpaper-default.jpg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/wallpapers/default.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/wallpapers/light.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/dist/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/eslint.config.js +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/info.md +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/package-lock.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/package.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/postcss.config.js +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/public/wallpaper-default.jpg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/public/wallpapers/default.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/public/wallpapers/light.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/public/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/public/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/App.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/AgentMonitor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ApiTester.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/AppRouter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ArchiveManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/AsciiArt.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Base64Tool.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Browser.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Calculator.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Calendar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Chat.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Chess.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Clock.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/CodeEditor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ColorPalette.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ColorPicker.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Contacts.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/DocumentViewer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Drawing.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Email.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/FileManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/FlappyBird.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/FtpClient.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Game2048.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/GitClient.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ImageGallery.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ImageViewer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/JsonFormatter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/MatrixRain.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/MediaConverter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Memory.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/MemoryManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Minesweeper.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/MusicPlayer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/NetworkTools.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Notes.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/PasswordManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/PhotoEditor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Pong.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/RegexTester.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Reminders.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/RssReader.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Settings.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Snake.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Solitaire.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Spreadsheet.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Sudoku.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/SystemMonitor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/TaskManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Terminal.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Tetris.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/TextEditor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/TicTacToe.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Todo.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/VideoPlayer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Weather.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/Whiteboard.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/apps/registry.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/AppContainer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/AppLauncher.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/BootSequence.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ContextMenu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/Desktop.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/Dock.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/LoginScreen.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/NotImplemented.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/NotificationCenter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/NotificationSystem.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/TopPanel.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/WindowFrame.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/WindowManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/accordion.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/alert.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/avatar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/badge.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/button-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/button.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/calendar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/card.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/carousel.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/chart.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/checkbox.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/collapsible.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/command.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/context-menu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/dialog.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/drawer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/empty.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/field.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/form.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/hover-card.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/input-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/input-otp.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/input.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/item.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/kbd.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/label.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/menubar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/pagination.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/popover.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/progress.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/radio-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/resizable.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/scroll-area.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/select.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/separator.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/sheet.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/sidebar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/skeleton.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/slider.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/sonner.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/spinner.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/switch.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/table.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/tabs.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/textarea.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/toggle-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/toggle.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/components/ui/tooltip.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/index.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/use-mobile.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusAgents.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusChat.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusEvents.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusHealth.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusMemory.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusSkills.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useDulusTasks.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useFileSystem.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useOSStore.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useSkillBridge.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useSystemBattery.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/hooks/useSystemVolume.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/index.css +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/lib/dulus-api.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/lib/utils.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/main.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/types/index.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/src/utils/assets.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/tailwind.config.js +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/tsconfig.app.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/tsconfig.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/tsconfig.node.json +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/sandbox/vite.config.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/setup.cfg +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skill/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skill/builtin.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skill/clawhub.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skill/executor.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skill/loader.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skill/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/skills.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/spinner.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/string_utils.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/subagent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/task/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/task/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/task/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/task/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_afk_yolo.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_approval_runtime.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_background_task_tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_background_tasks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_clipboard_utils.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_compaction.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_diff_view.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_diff_visualization.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_display_blocks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_export_import.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_hook_engine.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_license.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_mcp.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_memory.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_notification_manager.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_plugin.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_session_fork.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_shell_mode.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_skills.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_steer_input.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_subagent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_task.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_think_tool.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_todo_tool.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_todo_visualization.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_voice.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tests/test_wire_events.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tmux_offloader.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tmux_tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tool_registry.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/ui/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/ui/input.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/ui/render.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/voice/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/voice/keyterms.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/voice/recorder.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/voice/stt.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/voice/tts.py +0 -0
- {dulus-0.2.50 → dulus-0.2.52}/webchat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.52
|
|
4
4
|
Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
|
|
5
5
|
Author: KevRojo
|
|
6
6
|
License: GPL-3.0
|
|
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
69
69
|
<a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
|
|
70
70
|
<img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
|
|
71
71
|
<img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
|
|
72
|
-
<img src="https://img.shields.io/badge/version-v0.2.
|
|
72
|
+
<img src="https://img.shields.io/badge/version-v0.2.52-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
|
|
73
73
|
<img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
|
|
74
74
|
<img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
|
|
75
75
|
<img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
|
|
@@ -125,6 +125,7 @@ Dulus adapt any python repository <3
|
|
|
125
125
|
|
|
126
126
|
Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
|
|
127
127
|
|
|
128
|
+
> **v0.2.52 — May 12, 2026** — Session system overhaul: removed 50-session scan limit, metadata now includes message counts, eliminated daily prune from save path. **Recommendation:** wipe old sessions and start clean — only sessions, your memories stay intact.
|
|
128
129
|
> **v0.2.22 — May 9, 2026** — `/bg start` spawns one detached Dulus daemon that serves CLI (IPC), Web (browser at `127.0.0.1:5000`), and Telegram simultaneously — all sharing the same session. WebChat now defaults to **loopback-only** (opt-in to LAN exposure with `/webchat lan on`).
|
|
129
130
|
> **v0.2.21 — May 9, 2026** — Console-freeze fix: system prompt now tells the model SleepTimer is for user reminders only — pauses inside command pipelines must use `sleep N` inside the Bash command itself.
|
|
130
131
|
> **v0.2.20 — May 9, 2026** — Windows IPC port-collision fix: switched to `SO_EXCLUSIVEADDRUSE` so a second Dulus instance correctly cedes the port and acts as client. End-to-end tested.
|
|
@@ -22,7 +22,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
22
22
|
<a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
|
|
23
23
|
<img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
|
|
24
24
|
<img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
|
|
25
|
-
<img src="https://img.shields.io/badge/version-v0.2.
|
|
25
|
+
<img src="https://img.shields.io/badge/version-v0.2.52-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
|
|
26
26
|
<img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
|
|
27
27
|
<img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
|
|
28
28
|
<img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
|
|
@@ -78,6 +78,7 @@ Dulus adapt any python repository <3
|
|
|
78
78
|
|
|
79
79
|
Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
|
|
80
80
|
|
|
81
|
+
> **v0.2.52 — May 12, 2026** — Session system overhaul: removed 50-session scan limit, metadata now includes message counts, eliminated daily prune from save path. **Recommendation:** wipe old sessions and start clean — only sessions, your memories stay intact.
|
|
81
82
|
> **v0.2.22 — May 9, 2026** — `/bg start` spawns one detached Dulus daemon that serves CLI (IPC), Web (browser at `127.0.0.1:5000`), and Telegram simultaneously — all sharing the same session. WebChat now defaults to **loopback-only** (opt-in to LAN exposure with `/webchat lan on`).
|
|
82
83
|
> **v0.2.21 — May 9, 2026** — Console-freeze fix: system prompt now tells the model SleepTimer is for user reminders only — pauses inside command pipelines must use `sleep N` inside the Bash command itself.
|
|
83
84
|
> **v0.2.20 — May 9, 2026** — Windows IPC port-collision fix: switched to `SO_EXCLUSIVEADDRUSE` so a second Dulus instance correctly cedes the port and acts as client. End-to-end tested.
|
|
@@ -9,21 +9,21 @@
|
|
|
9
9
|
"project": {
|
|
10
10
|
"name": "Dulus Command Center",
|
|
11
11
|
"repo_stats": {
|
|
12
|
-
"files":
|
|
13
|
-
"lines":
|
|
12
|
+
"files": 432,
|
|
13
|
+
"lines": 205188,
|
|
14
14
|
"languages": {
|
|
15
15
|
".example": 5,
|
|
16
16
|
"no_ext": 1424,
|
|
17
|
-
".py":
|
|
17
|
+
".py": 60473,
|
|
18
18
|
".html": 14944,
|
|
19
19
|
".in": 3,
|
|
20
20
|
".toml": 129,
|
|
21
21
|
".md": 2161,
|
|
22
22
|
".txt": 463,
|
|
23
23
|
".lock": 1,
|
|
24
|
-
".json":
|
|
25
|
-
".whl":
|
|
26
|
-
".gz":
|
|
24
|
+
".json": 9513,
|
|
25
|
+
".whl": 21729,
|
|
26
|
+
".gz": 20908,
|
|
27
27
|
".svg": 1123,
|
|
28
28
|
".png": 31639,
|
|
29
29
|
".sh": 144,
|
|
@@ -38,54 +38,41 @@
|
|
|
38
38
|
},
|
|
39
39
|
"recent_commits": [
|
|
40
40
|
{
|
|
41
|
-
"hash": "
|
|
42
|
-
"subject": "
|
|
41
|
+
"hash": "7fc805e",
|
|
42
|
+
"subject": "chore: bump version 0.2.50 -> 0.2.51",
|
|
43
43
|
"author": "KevRojo",
|
|
44
44
|
"date": "2026-05-12"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
"hash": "
|
|
48
|
-
"subject": "fix:
|
|
47
|
+
"hash": "360badc",
|
|
48
|
+
"subject": "fix: GUI session responsiveness, sidebar incremental diff, async scan, default session IDs",
|
|
49
49
|
"author": "KevRojo",
|
|
50
50
|
"date": "2026-05-12"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
|
-
"hash": "
|
|
54
|
-
"subject": "
|
|
53
|
+
"hash": "fae4455",
|
|
54
|
+
"subject": "feat: MemoryManager overhaul, skill bridge polish, server sync",
|
|
55
55
|
"author": "KevRojo",
|
|
56
56
|
"date": "2026-05-12"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
"hash": "
|
|
60
|
-
"subject": "
|
|
59
|
+
"hash": "bf6455f",
|
|
60
|
+
"subject": "feat: skill-to-chat bridge, MemPalace search API, sandbox UX polish",
|
|
61
61
|
"author": "KevRojo",
|
|
62
62
|
"date": "2026-05-12"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
|
-
"hash": "
|
|
66
|
-
"subject": "
|
|
65
|
+
"hash": "9803f97",
|
|
66
|
+
"subject": "fix: include sandbox/src and sandbox/public in wheel",
|
|
67
67
|
"author": "KevRojo",
|
|
68
68
|
"date": "2026-05-12"
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
71
|
"recent_changes": [
|
|
72
|
-
".gitignore",
|
|
73
|
-
"backend/server.py",
|
|
74
72
|
"data/context.json",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"sandbox/dist/index.html",
|
|
79
|
-
"sandbox/src/apps/AppRouter.tsx",
|
|
80
|
-
"sandbox/src/apps/Chat.tsx",
|
|
81
|
-
"sandbox/src/apps/MemoryManager.tsx",
|
|
82
|
-
"sandbox/src/apps/SkillsLauncher.tsx",
|
|
83
|
-
"sandbox/src/hooks/index.ts",
|
|
84
|
-
"sandbox/src/hooks/useAutoOpenChat.ts",
|
|
85
|
-
"sandbox/src/hooks/useOSStore.tsx",
|
|
86
|
-
"sandbox/src/hooks/useSkillBridge.ts",
|
|
87
|
-
"sandbox/src/lib/dulus-api.ts",
|
|
88
|
-
"webchat_server.py"
|
|
73
|
+
"dulus.py",
|
|
74
|
+
"gui/session_utils.py",
|
|
75
|
+
"pyproject.toml"
|
|
89
76
|
]
|
|
90
77
|
},
|
|
91
78
|
"tasks": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.52
|
|
4
4
|
Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
|
|
5
5
|
Author: KevRojo
|
|
6
6
|
License: GPL-3.0
|
|
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
|
|
|
69
69
|
<a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
|
|
70
70
|
<img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
|
|
71
71
|
<img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
|
|
72
|
-
<img src="https://img.shields.io/badge/version-v0.2.
|
|
72
|
+
<img src="https://img.shields.io/badge/version-v0.2.52-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
|
|
73
73
|
<img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
|
|
74
74
|
<img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
|
|
75
75
|
<img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
|
|
@@ -125,6 +125,7 @@ Dulus adapt any python repository <3
|
|
|
125
125
|
|
|
126
126
|
Dulus is a **lightweight Python reimplementation of Claude Code** that isn't locked to Claude. It ships the whole loop — REPL, tool dispatch, streaming, context compaction, checkpoints, sub-agents, voice, Telegram bridge, MCP, plugins — in roughly **12K lines you can actually read**. Fork it. Bend it. Run it offline against Qwen on your M2.
|
|
127
127
|
|
|
128
|
+
> **v0.2.52 — May 12, 2026** — Session system overhaul: removed 50-session scan limit, metadata now includes message counts, eliminated daily prune from save path. **Recommendation:** wipe old sessions and start clean — only sessions, your memories stay intact.
|
|
128
129
|
> **v0.2.22 — May 9, 2026** — `/bg start` spawns one detached Dulus daemon that serves CLI (IPC), Web (browser at `127.0.0.1:5000`), and Telegram simultaneously — all sharing the same session. WebChat now defaults to **loopback-only** (opt-in to LAN exposure with `/webchat lan on`).
|
|
129
130
|
> **v0.2.21 — May 9, 2026** — Console-freeze fix: system prompt now tells the model SleepTimer is for user reminders only — pauses inside command pipelines must use `sleep N` inside the Bash command itself.
|
|
130
131
|
> **v0.2.20 — May 9, 2026** — Windows IPC port-collision fix: switched to `SO_EXCLUSIVEADDRUSE` so a second Dulus instance correctly cedes the port and acts as client. End-to-end tested.
|
|
@@ -1158,7 +1158,7 @@ def _atomic_write_json(path: Path, data) -> None:
|
|
|
1158
1158
|
def _save_roundtable_session(log: list, save_path=None):
|
|
1159
1159
|
"""Save the full roundtable session log to a JSON file.
|
|
1160
1160
|
|
|
1161
|
-
Sessions go under config.
|
|
1161
|
+
Sessions go under config.SESSIONS_DIR (~/.dulus/sessions/),
|
|
1162
1162
|
consistent with /save and other session artifacts. Pass an explicit
|
|
1163
1163
|
save_path to override (used to keep all turns of one debate in one file).
|
|
1164
1164
|
"""
|
|
@@ -1166,9 +1166,9 @@ def _save_roundtable_session(log: list, save_path=None):
|
|
|
1166
1166
|
return
|
|
1167
1167
|
if save_path is None:
|
|
1168
1168
|
from datetime import datetime as _dt
|
|
1169
|
-
from config import
|
|
1170
|
-
|
|
1171
|
-
save_path =
|
|
1169
|
+
from config import SESSIONS_DIR
|
|
1170
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1171
|
+
save_path = SESSIONS_DIR / f"round_table_{_dt.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
1172
1172
|
try:
|
|
1173
1173
|
_atomic_write_json(save_path, log)
|
|
1174
1174
|
ok(f"Sesión de Mesa Redonda guardada en: {save_path}")
|
|
@@ -1193,7 +1193,7 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
|
|
|
1193
1193
|
mode="full" → session_latest.json + daily/ copy + append to history.json (REPL default)
|
|
1194
1194
|
mode="daemon"→ only overwrite SESSIONS_DIR/session_<sid>.json, skip latest/history/daily.
|
|
1195
1195
|
"""
|
|
1196
|
-
from config import
|
|
1196
|
+
from config import DAILY_DIR, SESSION_HIST_FILE, SESSIONS_DIR
|
|
1197
1197
|
if not state.messages:
|
|
1198
1198
|
return True
|
|
1199
1199
|
|
|
@@ -1213,16 +1213,16 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
|
|
|
1213
1213
|
return True
|
|
1214
1214
|
|
|
1215
1215
|
# ── Full mode (REPL exit) ──
|
|
1216
|
-
daily_limit = cfg.get("
|
|
1217
|
-
history_limit = cfg.get("
|
|
1216
|
+
daily_limit = cfg.get("session_limit_daily", 10)
|
|
1217
|
+
history_limit = cfg.get("session_limit_history", 200)
|
|
1218
1218
|
|
|
1219
1219
|
now = datetime.now()
|
|
1220
1220
|
ts = now.strftime("%H%M%S")
|
|
1221
1221
|
date_str = now.strftime("%Y-%m-%d")
|
|
1222
1222
|
|
|
1223
1223
|
# 1. session_latest.json — always overwrite for quick /resume
|
|
1224
|
-
|
|
1225
|
-
latest_path =
|
|
1224
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1225
|
+
latest_path = SESSIONS_DIR / "session_latest.json"
|
|
1226
1226
|
latest_path.write_text(payload)
|
|
1227
1227
|
|
|
1228
1228
|
# 2. daily/YYYY-MM-DD/session_HHMMSS_sid.json
|
|
@@ -1239,11 +1239,6 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
|
|
|
1239
1239
|
daily_path = day_dir / f"session_{ts}_{sid}.json"
|
|
1240
1240
|
daily_path.write_text(payload)
|
|
1241
1241
|
|
|
1242
|
-
# Prune daily folder: keep only the latest `daily_limit` files
|
|
1243
|
-
daily_files = sorted(day_dir.glob("session_*.json"))
|
|
1244
|
-
for old in daily_files[:-daily_limit]:
|
|
1245
|
-
old.unlink(missing_ok=True)
|
|
1246
|
-
|
|
1247
1242
|
# 3. Append to history.json (master file)
|
|
1248
1243
|
if SESSION_HIST_FILE.exists():
|
|
1249
1244
|
try:
|
|
@@ -1267,22 +1262,16 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
|
|
|
1267
1262
|
ok(f" → {SESSION_HIST_FILE} ({len(hist['sessions'])} sessions / {hist['total_turns']} total turns)")
|
|
1268
1263
|
return True
|
|
1269
1264
|
def cmd_load(args: str, state, config) -> bool:
|
|
1270
|
-
from config import SESSIONS_DIR,
|
|
1265
|
+
from config import SESSIONS_DIR, DAILY_DIR
|
|
1271
1266
|
|
|
1272
1267
|
path = None
|
|
1273
1268
|
if not args.strip():
|
|
1274
|
-
# Collect sessions from daily/ folders
|
|
1269
|
+
# Collect sessions from daily/ folders only (single source of truth for listing)
|
|
1275
1270
|
sessions: list[Path] = []
|
|
1276
1271
|
if DAILY_DIR.exists():
|
|
1277
1272
|
for day_dir in sorted(DAILY_DIR.iterdir(), reverse=True):
|
|
1278
1273
|
if day_dir.is_dir():
|
|
1279
1274
|
sessions.extend(sorted(day_dir.glob("session_*.json"), reverse=True))
|
|
1280
|
-
# Fall back to legacy mr_sessions/ if daily/ is empty
|
|
1281
|
-
if not sessions and MR_SESSION_DIR.exists():
|
|
1282
|
-
sessions = [s for s in sorted(MR_SESSION_DIR.glob("*.json"), reverse=True)
|
|
1283
|
-
if s.name != "session_latest.json"]
|
|
1284
|
-
# Also include manually /save'd sessions from SESSIONS_DIR root
|
|
1285
|
-
sessions.extend(sorted(SESSIONS_DIR.glob("session_*.json"), reverse=True))
|
|
1286
1275
|
|
|
1287
1276
|
if not sessions:
|
|
1288
1277
|
info("No saved sessions found.")
|
|
@@ -1403,8 +1392,7 @@ def cmd_load(args: str, state, config) -> bool:
|
|
|
1403
1392
|
fname = args.strip()
|
|
1404
1393
|
path = Path(fname) if "/" in fname or "\\" in fname else SESSIONS_DIR / fname
|
|
1405
1394
|
if not path.exists() and ("/" not in fname and "\\" not in fname):
|
|
1406
|
-
for alt in [
|
|
1407
|
-
*(d / fname for d in DAILY_DIR.iterdir()
|
|
1395
|
+
for alt in [*(d / fname for d in DAILY_DIR.iterdir()
|
|
1408
1396
|
if DAILY_DIR.exists() and d.is_dir())]:
|
|
1409
1397
|
if alt.exists():
|
|
1410
1398
|
path = alt
|
|
@@ -1422,16 +1410,16 @@ def cmd_load(args: str, state, config) -> bool:
|
|
|
1422
1410
|
return True
|
|
1423
1411
|
|
|
1424
1412
|
def cmd_resume(args: str, state, config) -> bool:
|
|
1425
|
-
from config import
|
|
1413
|
+
from config import SESSIONS_DIR
|
|
1426
1414
|
|
|
1427
1415
|
if not args.strip():
|
|
1428
|
-
path =
|
|
1416
|
+
path = SESSIONS_DIR / "session_latest.json"
|
|
1429
1417
|
if not path.exists():
|
|
1430
1418
|
info("No auto-saved sessions found.")
|
|
1431
1419
|
return True
|
|
1432
1420
|
else:
|
|
1433
1421
|
fname = args.strip()
|
|
1434
|
-
path = Path(fname) if "/" in fname else
|
|
1422
|
+
path = Path(fname) if "/" in fname else SESSIONS_DIR / fname
|
|
1435
1423
|
|
|
1436
1424
|
if not path.exists():
|
|
1437
1425
|
err(f"File not found: {path}")
|
|
@@ -7,8 +7,10 @@ Usage:
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
|
+
import json
|
|
10
11
|
import queue
|
|
11
12
|
import sys
|
|
13
|
+
import threading
|
|
12
14
|
import traceback
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
from typing import Callable
|
|
@@ -27,7 +29,7 @@ from gui.themes import get_theme, set_theme
|
|
|
27
29
|
from gui.session_utils import scan_sessions
|
|
28
30
|
|
|
29
31
|
# Session directories
|
|
30
|
-
from config import SESSIONS_DIR, DAILY_DIR
|
|
32
|
+
from config import SESSIONS_DIR, DAILY_DIR
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -163,6 +165,35 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
|
|
|
163
165
|
# Wire bridge into sidebar so context bar / model list work
|
|
164
166
|
app.sidebar.bridge = bridge
|
|
165
167
|
|
|
168
|
+
# ── Sidebar refresh (non-blocking) ────────────────────────────────────────
|
|
169
|
+
_sidebar_refresh_pending = False
|
|
170
|
+
|
|
171
|
+
def _refresh_sidebar_async() -> None:
|
|
172
|
+
"""Run scan_sessions in a background thread so the UI never freezes."""
|
|
173
|
+
nonlocal _sidebar_refresh_pending
|
|
174
|
+
if _sidebar_refresh_pending:
|
|
175
|
+
return
|
|
176
|
+
_sidebar_refresh_pending = True
|
|
177
|
+
|
|
178
|
+
def _do_scan():
|
|
179
|
+
try:
|
|
180
|
+
data = scan_sessions()
|
|
181
|
+
# Update UI from main thread
|
|
182
|
+
app.after(0, lambda: app.set_sessions(data))
|
|
183
|
+
finally:
|
|
184
|
+
nonlocal _sidebar_refresh_pending
|
|
185
|
+
_sidebar_refresh_pending = False
|
|
186
|
+
|
|
187
|
+
threading.Thread(target=_do_scan, daemon=True).start()
|
|
188
|
+
|
|
189
|
+
def _load_session_messages(path: str) -> list[dict]:
|
|
190
|
+
"""Load messages directly from a session file."""
|
|
191
|
+
try:
|
|
192
|
+
data = json.loads(Path(path).read_text(encoding="utf-8", errors="replace"))
|
|
193
|
+
return data.get("messages", [])
|
|
194
|
+
except Exception:
|
|
195
|
+
return []
|
|
196
|
+
|
|
166
197
|
# ── Wire callbacks ────────────────────────────────────────────────────────
|
|
167
198
|
|
|
168
199
|
def _on_send(text: str) -> None:
|
|
@@ -176,8 +207,8 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
|
|
|
176
207
|
sid = bridge.save_current_session()
|
|
177
208
|
if sid:
|
|
178
209
|
# If a new session was created, refresh sidebar to show it
|
|
179
|
-
|
|
180
|
-
|
|
210
|
+
_refresh_sidebar_async()
|
|
211
|
+
|
|
181
212
|
app.hide_thinking()
|
|
182
213
|
app.chat.clear_chat()
|
|
183
214
|
bridge.clear_session()
|
|
@@ -188,36 +219,38 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
|
|
|
188
219
|
def _on_session_select(session_id: str) -> None:
|
|
189
220
|
# Save current session before switching to ensure no loss
|
|
190
221
|
sid = bridge.save_current_session()
|
|
191
|
-
|
|
222
|
+
|
|
192
223
|
# If we were in a new chat that just got saved, refresh sidebar to show it
|
|
193
224
|
if sid:
|
|
194
|
-
|
|
195
|
-
|
|
225
|
+
_refresh_sidebar_async()
|
|
226
|
+
|
|
196
227
|
app.hide_thinking()
|
|
197
|
-
|
|
198
|
-
# 1.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
228
|
+
|
|
229
|
+
# 1. Find the session file path (from cache or scan)
|
|
230
|
+
session_path = None
|
|
231
|
+
cached = app.sidebar._session_cache.get(session_id)
|
|
232
|
+
if cached:
|
|
233
|
+
session_path = cached.get("path")
|
|
234
|
+
if not session_path:
|
|
202
235
|
for s in scan_sessions():
|
|
203
236
|
if s["id"] == session_id:
|
|
204
|
-
|
|
237
|
+
session_path = s.get("path")
|
|
205
238
|
break
|
|
206
|
-
|
|
207
|
-
if not
|
|
239
|
+
|
|
240
|
+
if not session_path:
|
|
208
241
|
return
|
|
209
242
|
|
|
210
|
-
# 2.
|
|
211
|
-
messages =
|
|
243
|
+
# 2. Load messages directly from disk (avoids keeping all messages in memory)
|
|
244
|
+
messages = _load_session_messages(session_path)
|
|
212
245
|
app.chat.load_messages(messages)
|
|
213
|
-
|
|
246
|
+
|
|
214
247
|
# 3. Defer bridge loading until first message (user request)
|
|
215
248
|
bridge.pending_history = messages
|
|
216
249
|
bridge.session_id = session_id
|
|
217
250
|
# Important: clear actual AI state so it's fresh until sync
|
|
218
251
|
from agent import AgentState
|
|
219
252
|
bridge.state = AgentState()
|
|
220
|
-
|
|
253
|
+
|
|
221
254
|
app.set_active_session(session_id)
|
|
222
255
|
app.sidebar.update_context_bar()
|
|
223
256
|
app.set_status("Sesión lista (Contexto diferido)", t["success"])
|
|
@@ -236,8 +269,8 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
|
|
|
236
269
|
app.on_model_change = _on_model_change
|
|
237
270
|
app.on_session_select = _on_session_select
|
|
238
271
|
|
|
239
|
-
# Load existing sessions into sidebar
|
|
240
|
-
|
|
272
|
+
# Load existing sessions into sidebar (async so GUI shows immediately)
|
|
273
|
+
_refresh_sidebar_async()
|
|
241
274
|
app.sidebar._refresh_model_list()
|
|
242
275
|
app.sidebar.update_context_bar()
|
|
243
276
|
|
|
@@ -291,7 +324,7 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
|
|
|
291
324
|
# Rebuilding after every message causes annoying flicker.
|
|
292
325
|
sid = event.get("session_id")
|
|
293
326
|
if sid and sid not in app.sidebar._session_buttons:
|
|
294
|
-
|
|
327
|
+
_refresh_sidebar_async()
|
|
295
328
|
if sid:
|
|
296
329
|
app.set_active_session(sid)
|
|
297
330
|
|
|
@@ -162,9 +162,9 @@ class DulusBridge:
|
|
|
162
162
|
|
|
163
163
|
def _process_turn(self, user_message: str) -> None:
|
|
164
164
|
# Assign session_id immediately to prevent UI duplication during turn
|
|
165
|
+
# Use "default" as fallback so New Chat doesn't spawn random UUIDs
|
|
165
166
|
if not self.session_id:
|
|
166
|
-
|
|
167
|
-
self.session_id = uuid.uuid4().hex[:8]
|
|
167
|
+
self.session_id = "default"
|
|
168
168
|
|
|
169
169
|
# ── Skill inject (one-shot) ────────────────────────────────────────
|
|
170
170
|
skill_body = self._skill_inject
|
|
@@ -3,7 +3,11 @@ import json
|
|
|
3
3
|
import datetime
|
|
4
4
|
import uuid
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from config import SESSIONS_DIR, DAILY_DIR,
|
|
6
|
+
from config import SESSIONS_DIR, DAILY_DIR, SESSION_HIST_FILE
|
|
7
|
+
|
|
8
|
+
# File-mtime cache: path -> (mtime, result) to avoid re-reading unchanged files
|
|
9
|
+
_scan_cache: dict[str, tuple[float, dict]] = {}
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
def build_title(messages: list[dict]) -> str:
|
|
9
13
|
"""Generate a descriptive title from the first user message."""
|
|
@@ -15,64 +19,76 @@ def build_title(messages: list[dict]) -> str:
|
|
|
15
19
|
text = " ".join(part.get("text", "") for part in content if isinstance(part, dict))
|
|
16
20
|
else:
|
|
17
21
|
text = str(content)
|
|
18
|
-
|
|
22
|
+
|
|
19
23
|
if text.strip():
|
|
20
24
|
clean = text.strip().replace("\n", " ")
|
|
21
25
|
return clean[:40] + ("..." if len(clean) > 40 else "")
|
|
22
26
|
return "Nueva conversación"
|
|
23
27
|
|
|
28
|
+
|
|
29
|
+
def _read_session_meta(path: Path) -> dict | None:
|
|
30
|
+
"""Read session metadata with mtime caching."""
|
|
31
|
+
global _scan_cache
|
|
32
|
+
try:
|
|
33
|
+
mtime = path.stat().st_mtime
|
|
34
|
+
key = str(path)
|
|
35
|
+
if key in _scan_cache:
|
|
36
|
+
cached_mtime, cached = _scan_cache[key]
|
|
37
|
+
if cached_mtime == mtime:
|
|
38
|
+
return cached
|
|
39
|
+
|
|
40
|
+
data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
41
|
+
sid = data.get("session_id", path.stem)
|
|
42
|
+
messages = data.get("messages", [])
|
|
43
|
+
title = build_title(messages)
|
|
44
|
+
saved_at = data.get("saved_at", "")
|
|
45
|
+
if saved_at and len(saved_at) >= 19:
|
|
46
|
+
title = f"{saved_at[11:16]} {title}"
|
|
47
|
+
|
|
48
|
+
result = {
|
|
49
|
+
"id": sid,
|
|
50
|
+
"title": title,
|
|
51
|
+
"path": str(path),
|
|
52
|
+
"saved_at": saved_at,
|
|
53
|
+
"turn_count": data.get("turn_count", len(messages) // 2),
|
|
54
|
+
"messages": messages,
|
|
55
|
+
}
|
|
56
|
+
_scan_cache[key] = (mtime, result)
|
|
57
|
+
return result
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
24
62
|
def scan_sessions() -> list[dict]:
|
|
25
|
-
"""Scan session directories and return sorted list of metadata.
|
|
63
|
+
"""Scan daily session directories and return sorted list of metadata.
|
|
64
|
+
|
|
65
|
+
Single source of truth for listing: only daily/ folder is scanned.
|
|
66
|
+
Other locations (root sessions/, checkpoints) continue to exist for
|
|
67
|
+
internal use but are not listed to avoid duplicates.
|
|
68
|
+
"""
|
|
26
69
|
sessions: list[dict] = []
|
|
27
70
|
seen: set[str] = set()
|
|
28
71
|
files: list[Path] = []
|
|
29
72
|
|
|
30
|
-
# Daily sessions (newest first)
|
|
73
|
+
# Daily sessions only (newest first)
|
|
31
74
|
if DAILY_DIR.exists():
|
|
32
75
|
for day_dir in sorted(DAILY_DIR.iterdir(), reverse=True):
|
|
33
76
|
if day_dir.is_dir():
|
|
34
77
|
files.extend(sorted(day_dir.glob("session_*.json"), reverse=True))
|
|
35
78
|
|
|
36
|
-
# MR sessions
|
|
37
|
-
if MR_SESSION_DIR.exists():
|
|
38
|
-
files.extend(
|
|
39
|
-
s for s in sorted(MR_SESSION_DIR.glob("*.json"), reverse=True)
|
|
40
|
-
if s.name != "session_latest.json"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
# Root sessions
|
|
44
|
-
if SESSIONS_DIR.exists():
|
|
45
|
-
files.extend(sorted(SESSIONS_DIR.glob("session_*.json"), reverse=True))
|
|
46
|
-
|
|
47
79
|
for path in files:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
seen.add(sid)
|
|
54
|
-
|
|
55
|
-
messages = data.get("messages", [])
|
|
56
|
-
title = build_title(messages)
|
|
57
|
-
|
|
58
|
-
saved_at = data.get("saved_at", "")
|
|
59
|
-
if saved_at and len(saved_at) >= 19:
|
|
60
|
-
# Add time prefix: "HH:MM Title"
|
|
61
|
-
title = f"{saved_at[11:16]} {title}"
|
|
62
|
-
|
|
63
|
-
sessions.append({
|
|
64
|
-
"id": sid,
|
|
65
|
-
"title": title,
|
|
66
|
-
"path": str(path),
|
|
67
|
-
"messages": messages,
|
|
68
|
-
"saved_at": saved_at
|
|
69
|
-
})
|
|
70
|
-
except Exception:
|
|
80
|
+
meta = _read_session_meta(path)
|
|
81
|
+
if not meta:
|
|
82
|
+
continue
|
|
83
|
+
sid = meta["id"]
|
|
84
|
+
if sid in seen:
|
|
71
85
|
continue
|
|
86
|
+
seen.add(sid)
|
|
87
|
+
sessions.append(meta)
|
|
72
88
|
|
|
73
89
|
# Sort all found sessions by saved_at DESC
|
|
74
90
|
sessions.sort(key=lambda x: x.get("saved_at", ""), reverse=True)
|
|
75
|
-
return sessions
|
|
91
|
+
return sessions
|
|
76
92
|
|
|
77
93
|
def save_session(state, config: dict, session_id: str | None = None) -> str:
|
|
78
94
|
"""Save AgentState to disk in standard Dulus format. Returns the session_id."""
|
|
@@ -101,8 +117,8 @@ def save_session(state, config: dict, session_id: str | None = None) -> str:
|
|
|
101
117
|
payload = json.dumps(data, indent=2, default=str)
|
|
102
118
|
|
|
103
119
|
# 2. Save latest for /resume
|
|
104
|
-
|
|
105
|
-
(
|
|
120
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
(SESSIONS_DIR / "session_latest.json").write_text(payload, encoding="utf-8")
|
|
106
122
|
|
|
107
123
|
# 3. Save to daily folder
|
|
108
124
|
day_dir = DAILY_DIR / date_str
|
|
@@ -155,16 +171,8 @@ def delete_session(session_id: str) -> bool:
|
|
|
155
171
|
return False
|
|
156
172
|
|
|
157
173
|
deleted = False
|
|
158
|
-
|
|
159
|
-
# 1. Scan and delete in MR_SESSION_DIR (except latest maybe?)
|
|
160
|
-
if MR_SESSION_DIR.exists():
|
|
161
|
-
for p in MR_SESSION_DIR.glob(f"*{session_id}*"):
|
|
162
|
-
try:
|
|
163
|
-
p.unlink()
|
|
164
|
-
deleted = True
|
|
165
|
-
except: pass
|
|
166
174
|
|
|
167
|
-
#
|
|
175
|
+
# 1. Daily sessions
|
|
168
176
|
if DAILY_DIR.exists():
|
|
169
177
|
for d in DAILY_DIR.iterdir():
|
|
170
178
|
if d.is_dir():
|
|
@@ -174,7 +182,7 @@ def delete_session(session_id: str) -> bool:
|
|
|
174
182
|
deleted = True
|
|
175
183
|
except: pass
|
|
176
184
|
|
|
177
|
-
#
|
|
185
|
+
# 2. Root sessions (includes session_latest.json and manual /save files)
|
|
178
186
|
if SESSIONS_DIR.exists():
|
|
179
187
|
for p in SESSIONS_DIR.glob(f"*{session_id}*"):
|
|
180
188
|
try:
|
|
@@ -182,7 +190,7 @@ def delete_session(session_id: str) -> bool:
|
|
|
182
190
|
deleted = True
|
|
183
191
|
except: pass
|
|
184
192
|
|
|
185
|
-
#
|
|
193
|
+
# 3. Update history.json
|
|
186
194
|
if SESSION_HIST_FILE.exists():
|
|
187
195
|
try:
|
|
188
196
|
hist = json.loads(SESSION_HIST_FILE.read_text())
|