painapple-code 1.0.0__py3-none-any.whl
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.
- painapple_code/__init__.py +30 -0
- painapple_code/__main__.py +18 -0
- painapple_code/_version.py +24 -0
- painapple_code/auth_middleware.py +930 -0
- painapple_code/cli/__init__.py +204 -0
- painapple_code/cli/deploy/__init__.py +15 -0
- painapple_code/cli/deploy/claude_seed.py +41 -0
- painapple_code/cli/deploy/config.py +260 -0
- painapple_code/cli/deploy/container.py +715 -0
- painapple_code/cli/deploy/launch.py +163 -0
- painapple_code/cli/deploy/runtime.py +223 -0
- painapple_code/cli/lifecycle_cmd.py +443 -0
- painapple_code/cli/list_cmd.py +572 -0
- painapple_code/cli/manage_cmd.py +670 -0
- painapple_code/cli/netinfo.py +125 -0
- painapple_code/cli/profiles.py +390 -0
- painapple_code/cli/serve_args.py +163 -0
- painapple_code/cli/serve_config.py +256 -0
- painapple_code/cli/setup_cmd.py +664 -0
- painapple_code/cli/ui.py +438 -0
- painapple_code/cost_analytics.py +879 -0
- painapple_code/data/models.defaults.yaml +18 -0
- painapple_code/data/models.yaml +18 -0
- painapple_code/data/presets.defaults.json +32 -0
- painapple_code/data/strings.yaml +2269 -0
- painapple_code/helpers.py +406 -0
- painapple_code/paths.py +1514 -0
- painapple_code/prompt_explorer.py +984 -0
- painapple_code/providers/__init__.py +224 -0
- painapple_code/providers/base.py +710 -0
- painapple_code/providers/claude/__init__.py +74 -0
- painapple_code/providers/claude/capabilities.py +218 -0
- painapple_code/providers/claude/errors.py +79 -0
- painapple_code/providers/claude/launch.py +50 -0
- painapple_code/providers/claude/summary.py +89 -0
- painapple_code/providers/claude/translate.py +74 -0
- painapple_code/providers/claude_sdk/__init__.py +139 -0
- painapple_code/providers/claude_sdk/driver.py +412 -0
- painapple_code/providers/codex/__init__.py +87 -0
- painapple_code/providers/codex/capabilities.py +321 -0
- painapple_code/providers/codex/errors.py +70 -0
- painapple_code/providers/codex/launch.py +142 -0
- painapple_code/providers/codex/summary.py +196 -0
- painapple_code/providers/codex/translate.py +204 -0
- painapple_code/providers/codex_app_server/__init__.py +100 -0
- painapple_code/providers/codex_app_server/capabilities.py +41 -0
- painapple_code/providers/codex_app_server/launch.py +202 -0
- painapple_code/providers/codex_app_server/summary.py +291 -0
- painapple_code/providers/codex_app_server/translate.py +263 -0
- painapple_code/providers/codex_app_server/transport.py +238 -0
- painapple_code/routes/__init__.py +0 -0
- painapple_code/routes/api_agents.py +441 -0
- painapple_code/routes/api_app.py +368 -0
- painapple_code/routes/api_app_commit_sections.py +127 -0
- painapple_code/routes/api_app_config.py +997 -0
- painapple_code/routes/api_app_session_prefs.py +217 -0
- painapple_code/routes/api_browser.py +690 -0
- painapple_code/routes/api_commands.py +374 -0
- painapple_code/routes/api_costs.py +128 -0
- painapple_code/routes/api_drafts.py +139 -0
- painapple_code/routes/api_exec.py +149 -0
- painapple_code/routes/api_files.py +542 -0
- painapple_code/routes/api_git.py +657 -0
- painapple_code/routes/api_logs.py +649 -0
- painapple_code/routes/api_plugins.py +275 -0
- painapple_code/routes/api_project_config.py +418 -0
- painapple_code/routes/api_prompts.py +231 -0
- painapple_code/routes/api_providers.py +53 -0
- painapple_code/routes/api_search.py +411 -0
- painapple_code/routes/api_session_stash.py +234 -0
- painapple_code/routes/api_session_welcome.py +151 -0
- painapple_code/routes/api_sessions.py +742 -0
- painapple_code/routes/api_shadow.py +323 -0
- painapple_code/routes/api_shadow_db.py +512 -0
- painapple_code/routes/api_shadow_files.py +332 -0
- painapple_code/routes/api_shadow_search.py +476 -0
- painapple_code/routes/api_skills.py +610 -0
- painapple_code/routes/api_tasks.py +147 -0
- painapple_code/routes/api_terminal.py +540 -0
- painapple_code/routes/api_upload.py +293 -0
- painapple_code/routes/api_viewer.py +229 -0
- painapple_code/routes/dependencies.py +109 -0
- painapple_code/routes/ws_chat.py +837 -0
- painapple_code/server.py +2208 -0
- painapple_code/server_logging.py +263 -0
- painapple_code/services/__init__.py +0 -0
- painapple_code/services/agent_session.py +2738 -0
- painapple_code/session_store.py +253 -0
- painapple_code/session_store_core.py +879 -0
- painapple_code/shadow_db.py +830 -0
- painapple_code/shadow_db_plans.py +369 -0
- painapple_code/shadow_db_queries.py +595 -0
- painapple_code/shadow_db_schema.py +676 -0
- painapple_code/shadow_git.py +1296 -0
- painapple_code/shadow_git_frontmatter.py +314 -0
- painapple_code/shadow_git_sections.py +520 -0
- painapple_code/shadow_git_summary.py +462 -0
- painapple_code/shadow_parser.py +560 -0
- painapple_code/static/css/00-variables.css +276 -0
- painapple_code/static/css/01-base.css +245 -0
- painapple_code/static/css/02-tooltips.css +36 -0
- painapple_code/static/css/09-left-rail.css +448 -0
- painapple_code/static/css/10-header.css +777 -0
- painapple_code/static/css/11-connection-bar.css +120 -0
- painapple_code/static/css/20-messages.css +750 -0
- painapple_code/static/css/21-tool-blocks.css +2340 -0
- painapple_code/static/css/22-edit-diff.css +619 -0
- painapple_code/static/css/23-thinking.css +1584 -0
- painapple_code/static/css/24-background-tasks.css +359 -0
- painapple_code/static/css/25-context-block.css +252 -0
- painapple_code/static/css/26-session-setup.css +144 -0
- painapple_code/static/css/30-markdown.css +854 -0
- painapple_code/static/css/40-input.css +2418 -0
- painapple_code/static/css/41-autocomplete.css +127 -0
- painapple_code/static/css/42-activity-strip.css +97 -0
- painapple_code/static/css/43-file-autocomplete.css +121 -0
- painapple_code/static/css/44-snippets-autocomplete.css +224 -0
- painapple_code/static/css/45-selection-bar.css +433 -0
- painapple_code/static/css/46-keyboard-bar.css +119 -0
- painapple_code/static/css/47-keyboard-longpress.css +116 -0
- painapple_code/static/css/47-token-profile.css +106 -0
- painapple_code/static/css/50-welcome.css +3095 -0
- painapple_code/static/css/51-modals.css +301 -0
- painapple_code/static/css/52-utilities.css +77 -0
- painapple_code/static/css/53-image-upload.css +456 -0
- painapple_code/static/css/54-todo-list.css +102 -0
- painapple_code/static/css/55-file-upload.css +113 -0
- painapple_code/static/css/56-chat-navigator.css +218 -0
- painapple_code/static/css/56-permission-card.css +332 -0
- painapple_code/static/css/56-plan-approval.css +204 -0
- painapple_code/static/css/56-question-form.css +602 -0
- painapple_code/static/css/57-swipe-indicator.css +95 -0
- painapple_code/static/css/58-session-families.css +627 -0
- painapple_code/static/css/59-about.css +196 -0
- painapple_code/static/css/61-log-explorer.css +723 -0
- painapple_code/static/css/62-json-tree.css +489 -0
- painapple_code/static/css/62-skills-widget.css +650 -0
- painapple_code/static/css/63-commands-widget.css +30 -0
- painapple_code/static/css/63-plugins-widget.css +174 -0
- painapple_code/static/css/63-terminal.css +516 -0
- painapple_code/static/css/64-search-files-widget.css +448 -0
- painapple_code/static/css/65-snippets-widget.css +426 -0
- painapple_code/static/css/66-active-sessions.css +979 -0
- painapple_code/static/css/66-agents-widget.css +265 -0
- painapple_code/static/css/66-file-explorer-widget.css +855 -0
- painapple_code/static/css/66-git-panel.css +791 -0
- painapple_code/static/css/66-history-explorer.css +1496 -0
- painapple_code/static/css/67-diff-viewer.css +602 -0
- painapple_code/static/css/67-editor-view.css +748 -0
- painapple_code/static/css/67-file-preview-widget.css +1873 -0
- painapple_code/static/css/68-browser-widget.css +337 -0
- painapple_code/static/css/68-compare-wizard.css +355 -0
- painapple_code/static/css/68-csv-preview.css +106 -0
- painapple_code/static/css/68-diff-preview.css +103 -0
- painapple_code/static/css/68-helpers-install-widget.css +491 -0
- painapple_code/static/css/68-image-annotate.css +346 -0
- painapple_code/static/css/68-terminal-view.css +98 -0
- painapple_code/static/css/68-uploads-widget.css +229 -0
- painapple_code/static/css/69-cost-analytics.css +537 -0
- painapple_code/static/css/70-config-panel.css +3736 -0
- painapple_code/static/css/70-excalidraw.css +136 -0
- painapple_code/static/css/70-prompt-explorer.css +890 -0
- painapple_code/static/css/71-chart.css +111 -0
- painapple_code/static/css/72-discussion-sidebar.css +678 -0
- painapple_code/static/css/74-debug-logs.css +656 -0
- painapple_code/static/css/80-open-dialog.css +146 -0
- painapple_code/static/css/80-quick-switcher.css +383 -0
- painapple_code/static/css/80-widget-system.css +1763 -0
- painapple_code/static/css/81-quick-actions.css +1020 -0
- painapple_code/static/css/82-context-popover.css +1233 -0
- painapple_code/static/css/83-context-menu.css +339 -0
- painapple_code/static/css/85-lazy-loading.css +102 -0
- painapple_code/static/css/86-zen-mode.css +708 -0
- painapple_code/static/css/87-grid-switcher.css +346 -0
- painapple_code/static/css/login.css +198 -0
- painapple_code/static/dist/app.bundle.js +7177 -0
- painapple_code/static/feature-triage.html +446 -0
- painapple_code/static/icons/apple-touch-icon.png +0 -0
- painapple_code/static/icons/favicon-32.png +0 -0
- painapple_code/static/icons/favicon.ico +0 -0
- painapple_code/static/icons/icon-128.png +0 -0
- painapple_code/static/icons/icon-144.png +0 -0
- painapple_code/static/icons/icon-152.png +0 -0
- painapple_code/static/icons/icon-180.png +0 -0
- painapple_code/static/icons/icon-192.png +0 -0
- painapple_code/static/icons/icon-384.png +0 -0
- painapple_code/static/icons/icon-512.png +0 -0
- painapple_code/static/icons/icon-72.png +0 -0
- painapple_code/static/icons/icon-96.png +0 -0
- painapple_code/static/js/action-delegate.js +187 -0
- painapple_code/static/js/activity-strip.js +193 -0
- painapple_code/static/js/app/context-menus.js +619 -0
- painapple_code/static/js/app/file-preview.js +163 -0
- painapple_code/static/js/app/init-ui.js +574 -0
- painapple_code/static/js/app/input-events.js +561 -0
- painapple_code/static/js/app/input-resize.js +313 -0
- painapple_code/static/js/app/message-actions.js +419 -0
- painapple_code/static/js/app/panels.js +210 -0
- painapple_code/static/js/app/rendering-delegators.js +246 -0
- painapple_code/static/js/app/server-loading.js +365 -0
- painapple_code/static/js/app/session-ops.js +287 -0
- painapple_code/static/js/app/session-switch.js +443 -0
- painapple_code/static/js/app/settings-helpers.js +280 -0
- painapple_code/static/js/app/startup-welcome.js +290 -0
- painapple_code/static/js/app-context.js +157 -0
- painapple_code/static/js/app.js +2134 -0
- painapple_code/static/js/auth-fetch.js +46 -0
- painapple_code/static/js/background-tasks.js +274 -0
- painapple_code/static/js/boot.js +73 -0
- painapple_code/static/js/caret-position.js +171 -0
- painapple_code/static/js/chat-navigator.js +710 -0
- painapple_code/static/js/chat-search.js +323 -0
- painapple_code/static/js/command-executor.js +176 -0
- painapple_code/static/js/command-store.js +497 -0
- painapple_code/static/js/components.js +1414 -0
- painapple_code/static/js/config.js +218 -0
- painapple_code/static/js/context-menu.js +740 -0
- painapple_code/static/js/controllers/chat-controller.js +4252 -0
- painapple_code/static/js/controllers/dialog-controller.js +217 -0
- painapple_code/static/js/controllers/tab-controller.js +1828 -0
- painapple_code/static/js/controllers/thinking-controller.js +915 -0
- painapple_code/static/js/diff-utils.js +486 -0
- painapple_code/static/js/drafts-pill.js +95 -0
- painapple_code/static/js/editor-view.js +815 -0
- painapple_code/static/js/effort-settings.js +430 -0
- painapple_code/static/js/feature-triage.js +811 -0
- painapple_code/static/js/file-autocomplete.js +516 -0
- painapple_code/static/js/file-tabs.js +123 -0
- painapple_code/static/js/gestures.js +448 -0
- painapple_code/static/js/grid-switcher.js +334 -0
- painapple_code/static/js/input-handler.js +1472 -0
- painapple_code/static/js/keyboard-bar.js +495 -0
- painapple_code/static/js/layout-switcher.js +109 -0
- painapple_code/static/js/linkify-utils.js +200 -0
- painapple_code/static/js/login.js +141 -0
- painapple_code/static/js/open-dialog.js +879 -0
- painapple_code/static/js/orphan-terminals.js +373 -0
- painapple_code/static/js/path-utils.js +179 -0
- painapple_code/static/js/perf-marks.js +102 -0
- painapple_code/static/js/permission-settings.js +475 -0
- painapple_code/static/js/pointer-input.js +59 -0
- painapple_code/static/js/preview/json-tree-menu.js +226 -0
- painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
- painapple_code/static/js/preview/json-tree.js +457 -0
- painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
- painapple_code/static/js/preview/preview-edit.js +367 -0
- painapple_code/static/js/preview/preview-events.js +361 -0
- painapple_code/static/js/preview/preview-history.js +567 -0
- painapple_code/static/js/preview/preview-inline-edit.js +869 -0
- painapple_code/static/js/preview/preview-poll.js +108 -0
- painapple_code/static/js/preview/preview-render.js +572 -0
- painapple_code/static/js/preview/preview-search.js +421 -0
- painapple_code/static/js/preview/preview-state.js +256 -0
- painapple_code/static/js/preview/preview-utils.js +251 -0
- painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
- painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
- painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
- painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
- painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
- painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
- painapple_code/static/js/preview-plugins/index.js +34 -0
- painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
- painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
- painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
- painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
- painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
- painapple_code/static/js/project-color-picker.js +110 -0
- painapple_code/static/js/project-colors.js +163 -0
- painapple_code/static/js/prompt-favorites.js +187 -0
- painapple_code/static/js/quick-actions-menu.js +1736 -0
- painapple_code/static/js/quick-actions-registry.js +1239 -0
- painapple_code/static/js/quick-switcher/controller.js +294 -0
- painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
- painapple_code/static/js/quick-switcher/index.js +30 -0
- painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
- painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
- painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
- painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
- painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
- painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
- painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
- painapple_code/static/js/quick-switcher/registry.js +69 -0
- painapple_code/static/js/quick-switcher/ui/item.js +54 -0
- painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
- painapple_code/static/js/recent-opens.js +40 -0
- painapple_code/static/js/scroll-manager.js +530 -0
- painapple_code/static/js/scroll-state-machine.js +359 -0
- painapple_code/static/js/selection/action-bar.js +793 -0
- painapple_code/static/js/selection/index.js +15 -0
- painapple_code/static/js/selection/selection-handler.js +877 -0
- painapple_code/static/js/selection/state.js +181 -0
- painapple_code/static/js/session/agent-progress.js +202 -0
- painapple_code/static/js/session/handle-agent-message.js +481 -0
- painapple_code/static/js/session/handle-message.js +550 -0
- painapple_code/static/js/session/interactive.js +587 -0
- painapple_code/static/js/session/message-store.js +271 -0
- painapple_code/static/js/session/messages.js +253 -0
- painapple_code/static/js/session/persistence.js +179 -0
- painapple_code/static/js/session/restore.js +136 -0
- painapple_code/static/js/session/sync.js +333 -0
- painapple_code/static/js/session-container-pool.js +371 -0
- painapple_code/static/js/session-setup-panel.js +274 -0
- painapple_code/static/js/session.js +1047 -0
- painapple_code/static/js/shortcut-hints.js +272 -0
- painapple_code/static/js/shortcuts.js +1146 -0
- painapple_code/static/js/skills-autocomplete.js +262 -0
- painapple_code/static/js/snippets-autocomplete.js +738 -0
- painapple_code/static/js/stash-refs-view.js +83 -0
- painapple_code/static/js/stash-ui.js +768 -0
- painapple_code/static/js/stash.js +785 -0
- painapple_code/static/js/status-bar.js +1184 -0
- painapple_code/static/js/token-profile.js +274 -0
- painapple_code/static/js/tool-renderer-blocks.js +1626 -0
- painapple_code/static/js/tool-renderer-thinking.js +813 -0
- painapple_code/static/js/tool-renderer.js +1500 -0
- painapple_code/static/js/tooltips.js +197 -0
- painapple_code/static/js/upload-manager.js +1019 -0
- painapple_code/static/js/utils.js +521 -0
- painapple_code/static/js/welcome/api.js +363 -0
- painapple_code/static/js/welcome/cards.js +626 -0
- painapple_code/static/js/welcome/context-menu.js +585 -0
- painapple_code/static/js/welcome/families.js +801 -0
- painapple_code/static/js/welcome/preview.js +234 -0
- painapple_code/static/js/welcome/state.js +129 -0
- painapple_code/static/js/welcome.js +1874 -0
- painapple_code/static/js/widget-system/base-widget.js +717 -0
- painapple_code/static/js/widget-system/device-manager.js +178 -0
- painapple_code/static/js/widget-system/event-bus.js +152 -0
- painapple_code/static/js/widget-system/icons.js +120 -0
- painapple_code/static/js/widget-system/index.js +49 -0
- painapple_code/static/js/widget-system/init.js +54 -0
- painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
- painapple_code/static/js/widget-system/types/floating.js +915 -0
- painapple_code/static/js/widget-system/types/index.js +40 -0
- painapple_code/static/js/widget-system/types/modal.js +147 -0
- painapple_code/static/js/widget-system/types/sidebar.js +158 -0
- painapple_code/static/js/widget-system/types/tab.js +146 -0
- painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
- painapple_code/static/js/widget-system/widget-manager.js +1187 -0
- painapple_code/static/js/widgets/about-widget.js +260 -0
- painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
- painapple_code/static/js/widgets/agents-widget.js +897 -0
- painapple_code/static/js/widgets/browser-widget.js +579 -0
- painapple_code/static/js/widgets/commands-widget.js +694 -0
- painapple_code/static/js/widgets/config/commit-sections.js +449 -0
- painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
- painapple_code/static/js/widgets/config/gestures.js +161 -0
- painapple_code/static/js/widgets/config/models-tab.js +988 -0
- painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
- painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
- painapple_code/static/js/widgets/config/state.js +517 -0
- painapple_code/static/js/widgets/config/system-controls.js +90 -0
- painapple_code/static/js/widgets/config-widget.js +1535 -0
- painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
- painapple_code/static/js/widgets/debug-widget.js +693 -0
- painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
- painapple_code/static/js/widgets/discussion-widget.js +1107 -0
- painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
- painapple_code/static/js/widgets/file-preview-widget.js +861 -0
- painapple_code/static/js/widgets/git-widget.js +794 -0
- painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
- painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
- painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
- painapple_code/static/js/widgets/image-preview-widget.js +692 -0
- painapple_code/static/js/widgets/index.js +176 -0
- painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
- painapple_code/static/js/widgets/plugins-widget.js +305 -0
- painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
- painapple_code/static/js/widgets/search-files-widget.js +529 -0
- painapple_code/static/js/widgets/skills-widget.js +843 -0
- painapple_code/static/js/widgets/snippets-widget.js +479 -0
- painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
- painapple_code/static/js/widgets/tasks-widget.js +379 -0
- painapple_code/static/js/widgets/terminal/connection.js +283 -0
- painapple_code/static/js/widgets/terminal/gestures.js +357 -0
- painapple_code/static/js/widgets/terminal/init.js +642 -0
- painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
- painapple_code/static/js/widgets/terminal/osc52.js +213 -0
- painapple_code/static/js/widgets/terminal/render.js +159 -0
- painapple_code/static/js/widgets/terminal/size.js +54 -0
- painapple_code/static/js/widgets/terminal/state.js +211 -0
- painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
- painapple_code/static/js/widgets/terminal-widget.js +793 -0
- painapple_code/static/js/widgets/uploads-widget.js +461 -0
- painapple_code/static/js/widgets/zen-widget.js +1001 -0
- painapple_code/static/login.html +51 -0
- painapple_code/static/manifest.json +73 -0
- painapple_code/static/sw.js +293 -0
- painapple_code/static/vendor/LICENSES.txt +769 -0
- painapple_code/static/vendor/README.md +103 -0
- painapple_code/static/vendor/codemirror.js +35 -0
- painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
- painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
- painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
- painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
- painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
- painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
- painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
- painapple_code/static/vendor/highlight.min.js +1213 -0
- painapple_code/static/vendor/marked.min.js +6 -0
- painapple_code/static/vendor/purify.min.js +3 -0
- painapple_code/static/vendor/xterm-addon-fit.js +2 -0
- painapple_code/static/vendor/xterm.css +209 -0
- painapple_code/static/vendor/xterm.js +2 -0
- painapple_code/static/web-client.html +635 -0
- painapple_code/subprocess_registry.py +535 -0
- painapple_code/tls_cert.py +79 -0
- painapple_code/tools/agents/shadow-git-helper.md +400 -0
- painapple_code/tools/excalidraw-to-svg.js +143 -0
- painapple_code/tools/install-helpers.sh +148 -0
- painapple_code/tools/shadow-git +320 -0
- painapple_code/tools/shadow-query +120 -0
- painapple_code/tools/vegalite-to-svg.js +79 -0
- painapple_code/turn_query.py +766 -0
- painapple_code/turn_tracker.py +221 -0
- painapple_code/utils/__init__.py +0 -0
- painapple_code/utils/agent_cli.py +306 -0
- painapple_code/utils/chart.py +103 -0
- painapple_code/utils/excalidraw.py +107 -0
- painapple_code/utils/file_lock.py +162 -0
- painapple_code/utils/file_paths.py +648 -0
- painapple_code/utils/generate_icons.py +251 -0
- painapple_code/utils/proc.py +205 -0
- painapple_code/utils/pty_backend.py +59 -0
- painapple_code/utils/pty_posix.py +160 -0
- painapple_code/utils/pty_windows.py +245 -0
- painapple_code/utils/token_profiles.py +91 -0
- painapple_code/viewer_templates.py +657 -0
- painapple_code/welcome_search.py +879 -0
- painapple_code-1.0.0.dist-info/METADATA +292 -0
- painapple_code-1.0.0.dist-info/RECORD +436 -0
- painapple_code-1.0.0.dist-info/WHEEL +5 -0
- painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
- painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
- painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
- painapple_code-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"""Schema DDL, migrations, and FTS index management for ShadowDB.
|
|
2
|
+
|
|
3
|
+
Mixed into ShadowDB. Owns SCHEMA_SQL and the system field definitions.
|
|
4
|
+
Methods here run at DB init time, on recovery, or to refresh FTS state.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import duckdb
|
|
11
|
+
|
|
12
|
+
from painapple_code import paths
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("painapple-code.shadow-db")
|
|
15
|
+
|
|
16
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
# System field definitions (seeded on init)
|
|
18
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
SYSTEM_FIELD_DEFS = [
|
|
21
|
+
{"key": "summary", "label": "Summary", "field_type": "text",
|
|
22
|
+
"prompt": "One sentence: what was accomplished", "display_order": 1},
|
|
23
|
+
{"key": "work_done", "label": "Work Done", "field_type": "list",
|
|
24
|
+
"prompt": "Bullet points of specific changes", "display_order": 2},
|
|
25
|
+
{"key": "learnings", "label": "Learnings", "field_type": "list",
|
|
26
|
+
"prompt": "Key insights discovered", "display_order": 3},
|
|
27
|
+
{"key": "context_for_resume", "label": "Context", "field_type": "text",
|
|
28
|
+
"prompt": "What someone needs to continue this work", "display_order": 4},
|
|
29
|
+
{"key": "commands", "label": "Commands", "field_type": "list",
|
|
30
|
+
"prompt": "Reusable shell commands worth remembering", "display_order": 6},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
# Schema DDL
|
|
36
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
|
|
38
|
+
SCHEMA_SQL = """
|
|
39
|
+
-- Sessions: container for turns
|
|
40
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
41
|
+
id VARCHAR PRIMARY KEY,
|
|
42
|
+
project_hash VARCHAR NOT NULL,
|
|
43
|
+
git_repo_hash VARCHAR,
|
|
44
|
+
name VARCHAR,
|
|
45
|
+
created_at TIMESTAMPTZ,
|
|
46
|
+
updated_at TIMESTAMPTZ,
|
|
47
|
+
model VARCHAR,
|
|
48
|
+
archived BOOLEAN DEFAULT false,
|
|
49
|
+
total_cost DECIMAL(10,4) DEFAULT 0,
|
|
50
|
+
turn_count INTEGER DEFAULT 0,
|
|
51
|
+
provider VARCHAR DEFAULT 'claude' -- CLI agent backing the session
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_hash);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_repo ON sessions(git_repo_hash);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
|
|
57
|
+
|
|
58
|
+
-- Turns: THE HERO TABLE
|
|
59
|
+
CREATE TABLE IF NOT EXISTS turns (
|
|
60
|
+
id VARCHAR PRIMARY KEY,
|
|
61
|
+
session_id VARCHAR NOT NULL,
|
|
62
|
+
project_hash VARCHAR NOT NULL,
|
|
63
|
+
git_repo_hash VARCHAR,
|
|
64
|
+
turn_number INTEGER,
|
|
65
|
+
status VARCHAR DEFAULT 'pending',
|
|
66
|
+
|
|
67
|
+
-- Prompt (captured immediately)
|
|
68
|
+
user_prompt VARCHAR NOT NULL,
|
|
69
|
+
has_images BOOLEAN DEFAULT false,
|
|
70
|
+
has_files BOOLEAN DEFAULT false,
|
|
71
|
+
started_at TIMESTAMPTZ DEFAULT now(),
|
|
72
|
+
|
|
73
|
+
-- Git context (captured on send)
|
|
74
|
+
git_branch VARCHAR,
|
|
75
|
+
shadow_branch VARCHAR,
|
|
76
|
+
|
|
77
|
+
-- Completion (filled after turn finishes)
|
|
78
|
+
completed_at TIMESTAMPTZ,
|
|
79
|
+
git_hash VARCHAR,
|
|
80
|
+
cost DECIMAL(10,4),
|
|
81
|
+
summary_cost DECIMAL(10,4),
|
|
82
|
+
tokens_in UINTEGER,
|
|
83
|
+
tokens_out UINTEGER,
|
|
84
|
+
duration_ms INTEGER,
|
|
85
|
+
model VARCHAR,
|
|
86
|
+
|
|
87
|
+
-- Provider + result metadata
|
|
88
|
+
provider VARCHAR DEFAULT 'claude', -- CLI agent backing the turn
|
|
89
|
+
provider_session_id VARCHAR,
|
|
90
|
+
provider_turn_uuid VARCHAR,
|
|
91
|
+
result_subtype VARCHAR,
|
|
92
|
+
is_error BOOLEAN DEFAULT false,
|
|
93
|
+
num_tool_loops INTEGER,
|
|
94
|
+
duration_api_ms INTEGER,
|
|
95
|
+
service_tier VARCHAR,
|
|
96
|
+
|
|
97
|
+
-- Cache efficiency
|
|
98
|
+
cache_read_tokens UINTEGER,
|
|
99
|
+
cache_creation_tokens UINTEGER,
|
|
100
|
+
|
|
101
|
+
-- Web tool usage
|
|
102
|
+
web_search_count INTEGER DEFAULT 0,
|
|
103
|
+
web_fetch_count INTEGER DEFAULT 0,
|
|
104
|
+
|
|
105
|
+
-- Error tracking
|
|
106
|
+
error_message VARCHAR,
|
|
107
|
+
permission_denials VARCHAR[]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_turns_project ON turns(project_hash);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_turns_repo ON turns(git_repo_hash);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_turns_started ON turns(started_at DESC);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_turns_status ON turns(status);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_turns_git ON turns(git_hash);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_turns_branch ON turns(git_branch);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_turns_provider_session ON turns(provider_session_id);
|
|
118
|
+
|
|
119
|
+
-- Field definitions (system + user)
|
|
120
|
+
CREATE TABLE IF NOT EXISTS field_defs (
|
|
121
|
+
id INTEGER PRIMARY KEY,
|
|
122
|
+
project_hash VARCHAR,
|
|
123
|
+
key VARCHAR NOT NULL,
|
|
124
|
+
label VARCHAR NOT NULL,
|
|
125
|
+
field_type VARCHAR NOT NULL,
|
|
126
|
+
prompt VARCHAR,
|
|
127
|
+
display_order INTEGER DEFAULT 0,
|
|
128
|
+
source VARCHAR DEFAULT 'user',
|
|
129
|
+
enabled BOOLEAN DEFAULT true,
|
|
130
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
131
|
+
UNIQUE(project_hash, key)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
-- Turn fields: one row per field per turn
|
|
135
|
+
CREATE TABLE IF NOT EXISTS turn_fields (
|
|
136
|
+
id INTEGER PRIMARY KEY,
|
|
137
|
+
turn_id VARCHAR NOT NULL,
|
|
138
|
+
field_key VARCHAR NOT NULL,
|
|
139
|
+
value_text VARCHAR,
|
|
140
|
+
value_list VARCHAR[],
|
|
141
|
+
search_text VARCHAR,
|
|
142
|
+
edited BOOLEAN DEFAULT false,
|
|
143
|
+
UNIQUE(turn_id, field_key)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_turn_fields_turn ON turn_fields(turn_id);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_turn_fields_key ON turn_fields(field_key);
|
|
148
|
+
|
|
149
|
+
-- Tags
|
|
150
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
151
|
+
id INTEGER PRIMARY KEY,
|
|
152
|
+
turn_id VARCHAR NOT NULL,
|
|
153
|
+
tag VARCHAR NOT NULL,
|
|
154
|
+
source VARCHAR DEFAULT 'auto',
|
|
155
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
156
|
+
UNIQUE(turn_id, tag)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_tags_turn ON tags(turn_id);
|
|
161
|
+
|
|
162
|
+
-- Files changed per turn
|
|
163
|
+
CREATE TABLE IF NOT EXISTS turn_files (
|
|
164
|
+
id INTEGER PRIMARY KEY,
|
|
165
|
+
turn_id VARCHAR NOT NULL,
|
|
166
|
+
file_path VARCHAR NOT NULL,
|
|
167
|
+
change_type VARCHAR,
|
|
168
|
+
UNIQUE(turn_id, file_path)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_turn_files_path ON turn_files(file_path);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_turn_files_turn ON turn_files(turn_id);
|
|
173
|
+
|
|
174
|
+
-- Tools used per turn
|
|
175
|
+
CREATE TABLE IF NOT EXISTS turn_tools (
|
|
176
|
+
id INTEGER PRIMARY KEY,
|
|
177
|
+
turn_id VARCHAR NOT NULL,
|
|
178
|
+
tool_name VARCHAR NOT NULL,
|
|
179
|
+
call_count INTEGER DEFAULT 1,
|
|
180
|
+
UNIQUE(turn_id, tool_name)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_turn_tools_name ON turn_tools(tool_name);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_turn_tools_turn ON turn_tools(turn_id);
|
|
185
|
+
|
|
186
|
+
-- Per-model token/cost breakdown
|
|
187
|
+
CREATE TABLE IF NOT EXISTS turn_model_usage (
|
|
188
|
+
id INTEGER PRIMARY KEY,
|
|
189
|
+
turn_id VARCHAR NOT NULL,
|
|
190
|
+
model_id VARCHAR NOT NULL,
|
|
191
|
+
model_name VARCHAR,
|
|
192
|
+
input_tokens UINTEGER,
|
|
193
|
+
output_tokens UINTEGER,
|
|
194
|
+
cache_read_tokens UINTEGER,
|
|
195
|
+
cache_creation_tokens UINTEGER,
|
|
196
|
+
cost DECIMAL(10,6),
|
|
197
|
+
context_window INTEGER,
|
|
198
|
+
web_search_count INTEGER DEFAULT 0,
|
|
199
|
+
UNIQUE(turn_id, model_id)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_turn ON turn_model_usage(turn_id);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_model ON turn_model_usage(model_id);
|
|
204
|
+
|
|
205
|
+
-- Attachments (images, files) sent with prompts
|
|
206
|
+
CREATE TABLE IF NOT EXISTS turn_attachments (
|
|
207
|
+
id INTEGER PRIMARY KEY,
|
|
208
|
+
turn_id VARCHAR NOT NULL,
|
|
209
|
+
attachment_type VARCHAR NOT NULL,
|
|
210
|
+
file_path VARCHAR,
|
|
211
|
+
file_name VARCHAR,
|
|
212
|
+
media_type VARCHAR,
|
|
213
|
+
size_bytes INTEGER,
|
|
214
|
+
storage_path VARCHAR
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_turn ON turn_attachments(turn_id);
|
|
218
|
+
|
|
219
|
+
-- Context snapshots (token usage from /context)
|
|
220
|
+
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
221
|
+
id INTEGER PRIMARY KEY,
|
|
222
|
+
session_id VARCHAR NOT NULL,
|
|
223
|
+
turn_id VARCHAR,
|
|
224
|
+
captured_at TIMESTAMPTZ DEFAULT now(),
|
|
225
|
+
trigger VARCHAR NOT NULL,
|
|
226
|
+
model VARCHAR,
|
|
227
|
+
total_tokens INTEGER,
|
|
228
|
+
context_window INTEGER,
|
|
229
|
+
usage_pct INTEGER,
|
|
230
|
+
system_prompt_tokens INTEGER,
|
|
231
|
+
conversation_tokens INTEGER,
|
|
232
|
+
files_tokens INTEGER,
|
|
233
|
+
tools_tokens INTEGER,
|
|
234
|
+
other_tokens INTEGER
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
CREATE INDEX IF NOT EXISTS idx_context_session ON context_snapshots(session_id);
|
|
238
|
+
CREATE INDEX IF NOT EXISTS idx_context_turn ON context_snapshots(turn_id);
|
|
239
|
+
|
|
240
|
+
-- Compaction events
|
|
241
|
+
CREATE TABLE IF NOT EXISTS compactions (
|
|
242
|
+
id INTEGER PRIMARY KEY,
|
|
243
|
+
session_id VARCHAR NOT NULL,
|
|
244
|
+
triggered_at TIMESTAMPTZ DEFAULT now(),
|
|
245
|
+
trigger VARCHAR,
|
|
246
|
+
trigger_turn_id VARCHAR,
|
|
247
|
+
pre_tokens INTEGER,
|
|
248
|
+
post_tokens INTEGER,
|
|
249
|
+
summary VARCHAR,
|
|
250
|
+
turns_compacted INTEGER
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_compactions_session ON compactions(session_id);
|
|
254
|
+
|
|
255
|
+
-- Raw messages (optional, for deep FTS)
|
|
256
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
257
|
+
id INTEGER PRIMARY KEY,
|
|
258
|
+
session_id VARCHAR NOT NULL,
|
|
259
|
+
turn_id VARCHAR,
|
|
260
|
+
role VARCHAR NOT NULL,
|
|
261
|
+
content VARCHAR,
|
|
262
|
+
timestamp TIMESTAMPTZ
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
266
|
+
CREATE INDEX IF NOT EXISTS idx_messages_turn ON messages(turn_id);
|
|
267
|
+
|
|
268
|
+
-- Plans: first-class plan tracking
|
|
269
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
270
|
+
id VARCHAR PRIMARY KEY,
|
|
271
|
+
turn_id VARCHAR NOT NULL UNIQUE, -- The turn that created the plan
|
|
272
|
+
session_id VARCHAR NOT NULL,
|
|
273
|
+
project_hash VARCHAR NOT NULL,
|
|
274
|
+
git_repo_hash VARCHAR,
|
|
275
|
+
|
|
276
|
+
-- Plan identity
|
|
277
|
+
plan_file VARCHAR, -- e.g. ~/.claude/plans/zesty-prancing-hammock.md
|
|
278
|
+
plan_name VARCHAR, -- "zesty-prancing-hammock" (stem from path)
|
|
279
|
+
|
|
280
|
+
-- Content (short - full content read from file or shadow-git)
|
|
281
|
+
title VARCHAR, -- AI-generated: "Background Tasks Feature"
|
|
282
|
+
summary VARCHAR, -- AI-generated: 1-2 sentence description
|
|
283
|
+
|
|
284
|
+
-- Outcome tracking
|
|
285
|
+
status VARCHAR DEFAULT 'proposed', -- proposed | approved | rejected | abandoned
|
|
286
|
+
approved_at TIMESTAMPTZ,
|
|
287
|
+
|
|
288
|
+
-- Scope metrics (updated as implementation progresses)
|
|
289
|
+
implementation_turns INTEGER DEFAULT 0,
|
|
290
|
+
implementation_cost DECIMAL(10,4) DEFAULT 0,
|
|
291
|
+
files_changed INTEGER DEFAULT 0,
|
|
292
|
+
|
|
293
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
294
|
+
updated_at TIMESTAMPTZ DEFAULT now()
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
CREATE INDEX IF NOT EXISTS idx_plans_session ON plans(session_id);
|
|
298
|
+
CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_hash);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
300
|
+
CREATE INDEX IF NOT EXISTS idx_plans_repo ON plans(git_repo_hash);
|
|
301
|
+
|
|
302
|
+
-- Plan-turn links: which turns implemented a plan
|
|
303
|
+
CREATE TABLE IF NOT EXISTS plan_turns (
|
|
304
|
+
id INTEGER PRIMARY KEY,
|
|
305
|
+
plan_id VARCHAR NOT NULL,
|
|
306
|
+
turn_id VARCHAR NOT NULL,
|
|
307
|
+
role VARCHAR DEFAULT 'implementation', -- planning, implementation, verification
|
|
308
|
+
UNIQUE(plan_id, turn_id)
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_plan_turns_plan ON plan_turns(plan_id);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_plan_turns_turn ON plan_turns(turn_id);
|
|
313
|
+
|
|
314
|
+
-- File review acceptances: per-file review status within a session
|
|
315
|
+
CREATE TABLE IF NOT EXISTS file_acceptances (
|
|
316
|
+
id VARCHAR PRIMARY KEY,
|
|
317
|
+
session_id VARCHAR NOT NULL,
|
|
318
|
+
project_hash VARCHAR NOT NULL,
|
|
319
|
+
file_path VARCHAR NOT NULL,
|
|
320
|
+
shadow_commit VARCHAR NOT NULL,
|
|
321
|
+
status VARCHAR DEFAULT 'reviewed', -- 'reviewed' | 'flagged'
|
|
322
|
+
note TEXT,
|
|
323
|
+
created_at TIMESTAMPTZ DEFAULT current_timestamp,
|
|
324
|
+
updated_at TIMESTAMPTZ DEFAULT current_timestamp
|
|
325
|
+
);
|
|
326
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_acceptances_session_file
|
|
327
|
+
ON file_acceptances(session_id, file_path);
|
|
328
|
+
CREATE INDEX IF NOT EXISTS idx_acceptances_session
|
|
329
|
+
ON file_acceptances(session_id);
|
|
330
|
+
|
|
331
|
+
-- Projects: one row per local working-copy path. id = SHA256(abspath).
|
|
332
|
+
-- merged_into is reserved for the future merge widget that will let users
|
|
333
|
+
-- manually link two projects (e.g. two clones of the same repo) — until
|
|
334
|
+
-- that ships, all rows have merged_into IS NULL.
|
|
335
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
336
|
+
id VARCHAR PRIMARY KEY,
|
|
337
|
+
name VARCHAR NOT NULL,
|
|
338
|
+
merged_into VARCHAR,
|
|
339
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_projects_merged ON projects(merged_into);
|
|
343
|
+
|
|
344
|
+
-- Sequences for auto-increment IDs
|
|
345
|
+
CREATE SEQUENCE IF NOT EXISTS seq_field_defs START 1;
|
|
346
|
+
CREATE SEQUENCE IF NOT EXISTS seq_turn_fields START 1;
|
|
347
|
+
CREATE SEQUENCE IF NOT EXISTS seq_tags START 1;
|
|
348
|
+
CREATE SEQUENCE IF NOT EXISTS seq_turn_files START 1;
|
|
349
|
+
CREATE SEQUENCE IF NOT EXISTS seq_turn_tools START 1;
|
|
350
|
+
CREATE SEQUENCE IF NOT EXISTS seq_model_usage START 1;
|
|
351
|
+
CREATE SEQUENCE IF NOT EXISTS seq_attachments START 1;
|
|
352
|
+
CREATE SEQUENCE IF NOT EXISTS seq_context START 1;
|
|
353
|
+
CREATE SEQUENCE IF NOT EXISTS seq_compactions START 1;
|
|
354
|
+
CREATE SEQUENCE IF NOT EXISTS seq_messages START 1;
|
|
355
|
+
CREATE SEQUENCE IF NOT EXISTS seq_plan_turns START 1;
|
|
356
|
+
CREATE SEQUENCE IF NOT EXISTS seq_file_acceptances START 1;
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class _SchemaMixin:
|
|
361
|
+
"""Schema init, migrations, FTS, and index recovery for ShadowDB."""
|
|
362
|
+
|
|
363
|
+
def _recover_indexes(self) -> duckdb.DuckDBPyConnection:
|
|
364
|
+
"""Drop and recreate all indexes to fix ART corruption."""
|
|
365
|
+
logger.warning("Attempting ART index rebuild recovery...")
|
|
366
|
+
con = duckdb.connect(str(self.db_path))
|
|
367
|
+
# Extract CREATE INDEX statements from SCHEMA_SQL
|
|
368
|
+
index_stmts = []
|
|
369
|
+
for stmt in SCHEMA_SQL.split(";"):
|
|
370
|
+
s = stmt.strip()
|
|
371
|
+
if s.upper().startswith("CREATE INDEX"):
|
|
372
|
+
parts = s.split()
|
|
373
|
+
idx_name = parts[5] if "IF" in s.upper() else parts[2]
|
|
374
|
+
index_stmts.append((idx_name, s))
|
|
375
|
+
# Drop all indexes (safe — doesn't scan corrupt ART nodes)
|
|
376
|
+
for idx_name, _ in index_stmts:
|
|
377
|
+
try:
|
|
378
|
+
con.execute(f"DROP INDEX IF EXISTS {idx_name}")
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
# Recreate from clean data
|
|
382
|
+
for idx_name, stmt in index_stmts:
|
|
383
|
+
try:
|
|
384
|
+
con.execute(stmt)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.warning(f"Index {idx_name} recreate skipped: {e}")
|
|
387
|
+
logger.warning(f"Shadow DB recovered via ART index rebuild ({len(index_stmts)} indexes)")
|
|
388
|
+
return con
|
|
389
|
+
|
|
390
|
+
def _init_schema(self):
|
|
391
|
+
"""Initialize all tables and seed system field defs."""
|
|
392
|
+
con = self._con # Use raw connection (called from _get_con)
|
|
393
|
+
# Execute schema DDL (multiple statements)
|
|
394
|
+
for statement in SCHEMA_SQL.split(";"):
|
|
395
|
+
statement = statement.strip()
|
|
396
|
+
if statement:
|
|
397
|
+
try:
|
|
398
|
+
con.execute(statement)
|
|
399
|
+
except duckdb.FatalException:
|
|
400
|
+
raise # DB is dead, don't mask it
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.warning(f"Schema statement skipped: {e}")
|
|
403
|
+
|
|
404
|
+
# ── Migrations ──
|
|
405
|
+
# NOTE: DuckDB has a bug where ALTER TABLE ADD COLUMN in the WAL
|
|
406
|
+
# fails on replay ("GetDefaultDatabase with no default database set").
|
|
407
|
+
# Every migration that uses ALTER TABLE MUST call _migrate_add_column()
|
|
408
|
+
# which checkpoints immediately after each ALTER.
|
|
409
|
+
self._migrate_add_column(con, "sessions", "git_repo_hash", "VARCHAR")
|
|
410
|
+
self._migrate_add_column(con, "turns", "git_repo_hash", "VARCHAR")
|
|
411
|
+
self._migrate_add_column(con, "turns", "is_plan", "BOOLEAN DEFAULT false")
|
|
412
|
+
# Multi-provider readiness: existing rows predate other CLIs → all Claude.
|
|
413
|
+
self._migrate_add_column(con, "sessions", "provider", "VARCHAR DEFAULT 'claude'")
|
|
414
|
+
self._migrate_add_column(con, "turns", "provider", "VARCHAR DEFAULT 'claude'")
|
|
415
|
+
# Provider-neutral rename: the summary-fork cost column was haiku_cost.
|
|
416
|
+
self._migrate_rename_column(con, "turns", "haiku_cost", "summary_cost")
|
|
417
|
+
# Provider-neutral rename of the provider conversation/turn identity columns
|
|
418
|
+
# (claude_session_id predates multi-provider). The index is renamed too.
|
|
419
|
+
self._migrate_rename_column(con, "turns", "claude_session_id", "provider_session_id")
|
|
420
|
+
self._migrate_rename_column(con, "turns", "claude_turn_uuid", "provider_turn_uuid")
|
|
421
|
+
self._migrate_rename_index(con, "idx_turns_claude_session", "idx_turns_provider_session", "turns", "provider_session_id")
|
|
422
|
+
|
|
423
|
+
# Backfill plans from existing turn_tools data
|
|
424
|
+
self._backfill_plans(con)
|
|
425
|
+
|
|
426
|
+
# Backfill git_repo_hash for existing rows from project path files
|
|
427
|
+
nulls = con.execute(
|
|
428
|
+
"SELECT COUNT(*) FROM turns WHERE git_repo_hash IS NULL AND status != 'interrupted'"
|
|
429
|
+
).fetchone()[0]
|
|
430
|
+
if nulls > 0:
|
|
431
|
+
self._backfill_git_repo_hash(con)
|
|
432
|
+
|
|
433
|
+
# Seed system field definitions if empty
|
|
434
|
+
count = con.execute("SELECT COUNT(*) FROM field_defs WHERE source = 'system'").fetchone()[0]
|
|
435
|
+
if count == 0:
|
|
436
|
+
for fd in SYSTEM_FIELD_DEFS:
|
|
437
|
+
con.execute(
|
|
438
|
+
"""INSERT INTO field_defs (id, project_hash, key, label, field_type, prompt, display_order, source)
|
|
439
|
+
VALUES (nextval('seq_field_defs'), NULL, ?, ?, ?, ?, ?, 'system')""",
|
|
440
|
+
[fd["key"], fd["label"], fd["field_type"], fd["prompt"], fd["display_order"]]
|
|
441
|
+
)
|
|
442
|
+
logger.info(f"Seeded {len(SYSTEM_FIELD_DEFS)} system field definitions")
|
|
443
|
+
|
|
444
|
+
# Repair desynced sequences (e.g. after rebuild tool used manual IDs)
|
|
445
|
+
self._repair_sequences(con)
|
|
446
|
+
|
|
447
|
+
# Mark stale pending turns from previous server runs
|
|
448
|
+
stale = con.execute(
|
|
449
|
+
"UPDATE turns SET status = 'interrupted' WHERE status = 'pending' RETURNING id"
|
|
450
|
+
).fetchall()
|
|
451
|
+
if stale:
|
|
452
|
+
logger.info(f"Marked {len(stale)} stale pending turns as 'interrupted'")
|
|
453
|
+
|
|
454
|
+
# Full-text search setup
|
|
455
|
+
self._init_fts(con)
|
|
456
|
+
|
|
457
|
+
logger.info("Shadow DB schema initialized")
|
|
458
|
+
|
|
459
|
+
def _repair_sequences(self, con):
|
|
460
|
+
"""Ensure DuckDB sequences are ahead of MAX(id) in their tables.
|
|
461
|
+
|
|
462
|
+
The rebuild tool uses manual ID counters, which can leave sequences
|
|
463
|
+
behind the actual max ID. This causes 'Duplicate key' errors on
|
|
464
|
+
subsequent nextval() calls.
|
|
465
|
+
"""
|
|
466
|
+
seq_table_map = [
|
|
467
|
+
("seq_turn_fields", "turn_fields"),
|
|
468
|
+
("seq_tags", "tags"),
|
|
469
|
+
("seq_turn_files", "turn_files"),
|
|
470
|
+
("seq_turn_tools", "turn_tools"),
|
|
471
|
+
("seq_model_usage", "turn_model_usage"),
|
|
472
|
+
("seq_attachments", "turn_attachments"),
|
|
473
|
+
("seq_context", "context_snapshots"),
|
|
474
|
+
("seq_compactions", "compactions"),
|
|
475
|
+
("seq_messages", "messages"),
|
|
476
|
+
("seq_plan_turns", "plan_turns"),
|
|
477
|
+
]
|
|
478
|
+
# Read current sequence values from catalog (no side effects, unlike nextval)
|
|
479
|
+
try:
|
|
480
|
+
rows = con.execute(
|
|
481
|
+
"SELECT sequence_name, start_value FROM duckdb_sequences()"
|
|
482
|
+
).fetchall()
|
|
483
|
+
seq_values = {r[0]: r[1] for r in rows}
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.warning(f"Cannot read duckdb_sequences(), skipping repair: {e}")
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
repaired = []
|
|
489
|
+
for seq_name, table_name in seq_table_map:
|
|
490
|
+
try:
|
|
491
|
+
max_id = con.execute(f"SELECT COALESCE(MAX(id), 0) FROM {table_name}").fetchone()[0]
|
|
492
|
+
if max_id == 0:
|
|
493
|
+
continue
|
|
494
|
+
next_val = seq_values.get(seq_name)
|
|
495
|
+
if next_val is not None and next_val <= max_id:
|
|
496
|
+
target = max_id + 1
|
|
497
|
+
con.execute(f"DROP SEQUENCE {seq_name}")
|
|
498
|
+
con.execute(f"CREATE SEQUENCE {seq_name} START {target}")
|
|
499
|
+
repaired.append(f"{seq_name}: {next_val}→{target}")
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.warning(f"Sequence repair skipped for {seq_name}: {e}")
|
|
502
|
+
if repaired:
|
|
503
|
+
logger.warning(f"Repaired desynced sequences: {', '.join(repaired)}")
|
|
504
|
+
|
|
505
|
+
def _init_fts(self, con):
|
|
506
|
+
"""Load FTS extension and build indexes."""
|
|
507
|
+
try:
|
|
508
|
+
con.execute("INSTALL fts; LOAD fts;")
|
|
509
|
+
self._rebuild_fts_indexes(con)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.warning(f"FTS init failed (search will use ILIKE fallback): {e}")
|
|
512
|
+
self._fts_ready = False
|
|
513
|
+
|
|
514
|
+
def _rebuild_fts_indexes(self, con):
|
|
515
|
+
"""Build/rebuild FTS indexes on turns.user_prompt and turn_fields.search_text."""
|
|
516
|
+
t0 = time.monotonic()
|
|
517
|
+
try:
|
|
518
|
+
con.execute(
|
|
519
|
+
"PRAGMA create_fts_index('turns', 'id', 'user_prompt', "
|
|
520
|
+
"stemmer='porter', stopwords='english', overwrite=1)"
|
|
521
|
+
)
|
|
522
|
+
con.execute(
|
|
523
|
+
"PRAGMA create_fts_index('turn_fields', 'id', 'search_text', "
|
|
524
|
+
"stemmer='porter', stopwords='english', overwrite=1)"
|
|
525
|
+
)
|
|
526
|
+
self._fts_ready = True
|
|
527
|
+
self._fts_dirty = False
|
|
528
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
529
|
+
logger.info(f"FTS indexes built ({elapsed:.0f}ms)")
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.warning(f"FTS index build failed: {e}")
|
|
532
|
+
self._fts_ready = False
|
|
533
|
+
|
|
534
|
+
def rebuild_fts_if_needed(self):
|
|
535
|
+
"""Rebuild FTS indexes if data has changed since last build."""
|
|
536
|
+
if not self._fts_dirty:
|
|
537
|
+
return
|
|
538
|
+
with self._lock:
|
|
539
|
+
if not self._fts_dirty:
|
|
540
|
+
return
|
|
541
|
+
con = self._get_con()
|
|
542
|
+
try:
|
|
543
|
+
con.execute("LOAD fts;")
|
|
544
|
+
except Exception:
|
|
545
|
+
pass
|
|
546
|
+
self._rebuild_fts_indexes(con)
|
|
547
|
+
|
|
548
|
+
@staticmethod
|
|
549
|
+
def _migrate_add_column(con, table: str, column: str, col_type: str):
|
|
550
|
+
"""Add a column if it doesn't exist, with immediate checkpoint.
|
|
551
|
+
|
|
552
|
+
DuckDB WAL replay bug: ALTER TABLE in WAL fails on restart.
|
|
553
|
+
Checkpoint flushes the ALTER into the DB file so WAL never replays it.
|
|
554
|
+
"""
|
|
555
|
+
try:
|
|
556
|
+
con.execute(f"SELECT {column} FROM {table} LIMIT 0")
|
|
557
|
+
except duckdb.FatalException:
|
|
558
|
+
raise # DB is dead
|
|
559
|
+
except Exception:
|
|
560
|
+
con.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
|
|
561
|
+
con.execute("CHECKPOINT")
|
|
562
|
+
logger.info(f"Migration: added {table}.{column} ({col_type}) + checkpoint")
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def _migrate_rename_column(con, table: str, old: str, new: str):
|
|
566
|
+
"""Rename a column if the old name still exists, with immediate checkpoint.
|
|
567
|
+
|
|
568
|
+
Idempotent: skips when the new column already exists (fresh schema or
|
|
569
|
+
already migrated). CHECKPOINT after the ALTER works around DuckDB's
|
|
570
|
+
WAL-replay bug (the rename must not live only in the WAL on restart).
|
|
571
|
+
|
|
572
|
+
In DuckDB an FTS index AND the regular ART indexes on the table are
|
|
573
|
+
catalog dependencies that block RENAME COLUMN ("Cannot alter entry ...
|
|
574
|
+
because there are entries that depend on it"). So we capture each ART
|
|
575
|
+
index's CREATE SQL, drop them (and the FTS index), rename, then rebuild
|
|
576
|
+
the ART indexes from their saved SQL. _init_fts() rebuilds the FTS index
|
|
577
|
+
later in this same schema-init pass.
|
|
578
|
+
"""
|
|
579
|
+
try:
|
|
580
|
+
con.execute(f"SELECT {new} FROM {table} LIMIT 0")
|
|
581
|
+
return # new column already present — nothing to do
|
|
582
|
+
except duckdb.FatalException:
|
|
583
|
+
raise # DB is dead
|
|
584
|
+
except Exception:
|
|
585
|
+
pass
|
|
586
|
+
try:
|
|
587
|
+
con.execute(f"SELECT {old} FROM {table} LIMIT 0")
|
|
588
|
+
except duckdb.FatalException:
|
|
589
|
+
raise
|
|
590
|
+
except Exception:
|
|
591
|
+
return # neither column exists — let the CREATE TABLE DDL own it
|
|
592
|
+
|
|
593
|
+
# Capture ART index definitions so we can rebuild them after the rename.
|
|
594
|
+
index_sql = []
|
|
595
|
+
try:
|
|
596
|
+
for name, sql in con.execute(
|
|
597
|
+
"SELECT index_name, sql FROM duckdb_indexes() WHERE table_name = ?", [table]
|
|
598
|
+
).fetchall():
|
|
599
|
+
if sql:
|
|
600
|
+
index_sql.append((name, sql))
|
|
601
|
+
except Exception as e:
|
|
602
|
+
logger.warning(f"Could not enumerate indexes on {table}: {e}")
|
|
603
|
+
|
|
604
|
+
# Drop the FTS index (separate dependency) and every ART index.
|
|
605
|
+
try:
|
|
606
|
+
con.execute("LOAD fts;")
|
|
607
|
+
con.execute(f"PRAGMA drop_fts_index('{table}')")
|
|
608
|
+
except Exception:
|
|
609
|
+
pass # no FTS index on this table (or extension absent) — fine
|
|
610
|
+
for name, _ in index_sql:
|
|
611
|
+
try:
|
|
612
|
+
con.execute(f"DROP INDEX IF EXISTS {name}")
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.warning(f"Could not drop index {name}: {e}")
|
|
615
|
+
|
|
616
|
+
con.execute(f"ALTER TABLE {table} RENAME COLUMN {old} TO {new}")
|
|
617
|
+
con.execute("CHECKPOINT")
|
|
618
|
+
|
|
619
|
+
# Rebuild the ART indexes, rewriting any reference to the renamed column
|
|
620
|
+
# in the captured DDL (e.g. an index defined ON the column we just renamed).
|
|
621
|
+
for name, sql in index_sql:
|
|
622
|
+
try:
|
|
623
|
+
con.execute(sql.replace(old, new))
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.warning(f"Could not recreate index {name}: {e}")
|
|
626
|
+
con.execute("CHECKPOINT")
|
|
627
|
+
logger.info(
|
|
628
|
+
f"Migration: renamed {table}.{old} → {new} "
|
|
629
|
+
f"(dropped/rebuilt {len(index_sql)} ART indexes + FTS) + checkpoint"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
@staticmethod
|
|
633
|
+
def _migrate_rename_index(con, old_name: str, new_name: str, table: str, columns: str):
|
|
634
|
+
"""Rename an index (DuckDB has no RENAME INDEX): drop the old, create the
|
|
635
|
+
new. Idempotent + checkpointed. Used when a renamed column's index should
|
|
636
|
+
also shed the old name."""
|
|
637
|
+
try:
|
|
638
|
+
con.execute(f"DROP INDEX IF EXISTS {old_name}")
|
|
639
|
+
con.execute(f"CREATE INDEX IF NOT EXISTS {new_name} ON {table}({columns})")
|
|
640
|
+
con.execute("CHECKPOINT")
|
|
641
|
+
except duckdb.FatalException:
|
|
642
|
+
raise
|
|
643
|
+
except Exception as e:
|
|
644
|
+
logger.warning(f"Index rename {old_name} → {new_name} skipped: {e}")
|
|
645
|
+
|
|
646
|
+
def _backfill_git_repo_hash(self, con):
|
|
647
|
+
"""One-time backfill: compute git_repo_hash for existing rows."""
|
|
648
|
+
try:
|
|
649
|
+
from painapple_code.paths import get_git_repo_hash
|
|
650
|
+
|
|
651
|
+
# Build mapping from project_hash -> git_repo_hash via path files
|
|
652
|
+
projects_dir = paths.DATA_HOME / "projects"
|
|
653
|
+
if not projects_dir.exists():
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
mapping = {}
|
|
657
|
+
for h_dir in projects_dir.iterdir():
|
|
658
|
+
if h_dir.is_dir():
|
|
659
|
+
path_file = h_dir / "path"
|
|
660
|
+
if path_file.exists():
|
|
661
|
+
project_path = path_file.read_text(encoding="utf-8").strip()
|
|
662
|
+
repo_hash = get_git_repo_hash(project_path)
|
|
663
|
+
if repo_hash:
|
|
664
|
+
mapping[h_dir.name] = repo_hash
|
|
665
|
+
|
|
666
|
+
for ph, rh in mapping.items():
|
|
667
|
+
for table in ("turns", "sessions"):
|
|
668
|
+
rows = con.execute(
|
|
669
|
+
f"UPDATE {table} SET git_repo_hash = ? "
|
|
670
|
+
f"WHERE project_hash = ? AND git_repo_hash IS NULL RETURNING id",
|
|
671
|
+
[rh, ph]
|
|
672
|
+
).fetchall()
|
|
673
|
+
if rows:
|
|
674
|
+
logger.info(f"Backfill: set git_repo_hash={rh} on {len(rows)} {table} rows (project={ph})")
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.warning(f"git_repo_hash backfill failed (non-fatal): {e}")
|