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,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code CLI provider.
|
|
3
|
+
|
|
4
|
+
The reference implementation. Bodies here were moved (not rewritten) out of
|
|
5
|
+
`services/agent_session.py` and `utils/agent_cli.py`, so behavior is
|
|
6
|
+
identical — this is purely the relocation of Claude-specific logic behind the
|
|
7
|
+
`Provider` seam.
|
|
8
|
+
|
|
9
|
+
Claude's stream-json schema *is* the canonical internal event shape, so this
|
|
10
|
+
adapter is a near-identity: it inherits the base `translate_events` and only
|
|
11
|
+
normalizes cumulative cost to per-turn deltas. It's
|
|
12
|
+
assembled from focused mixins (this file is the combiner — see session_store.py
|
|
13
|
+
/ shadow_git.py for the same pattern):
|
|
14
|
+
|
|
15
|
+
capabilities.py → _CapabilitiesMixin binary, accounts, models, effort/perm
|
|
16
|
+
vocabularies, fetch_context,
|
|
17
|
+
skill/agent/plugin roots
|
|
18
|
+
launch.py → _LaunchMixin build_command (`claude -p …`)
|
|
19
|
+
translate.py → _TranslateMixin normalize_result (cumulative → delta)
|
|
20
|
+
summary.py → _SummaryMixin rich-commit summary fork (--fork-session)
|
|
21
|
+
errors.py → _ErrorsMixin stderr classification / retry policy
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from painapple_code.providers.base import Provider, Capabilities
|
|
27
|
+
from painapple_code.providers.claude.capabilities import _CapabilitiesMixin
|
|
28
|
+
from painapple_code.providers.claude.launch import _LaunchMixin
|
|
29
|
+
from painapple_code.providers.claude.translate import _TranslateMixin
|
|
30
|
+
from painapple_code.providers.claude.summary import _SummaryMixin
|
|
31
|
+
from painapple_code.providers.claude.errors import _ErrorsMixin
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ClaudeProvider(
|
|
35
|
+
_CapabilitiesMixin,
|
|
36
|
+
_LaunchMixin,
|
|
37
|
+
_TranslateMixin,
|
|
38
|
+
_SummaryMixin,
|
|
39
|
+
_ErrorsMixin,
|
|
40
|
+
Provider,
|
|
41
|
+
):
|
|
42
|
+
"""Adapter for the Claude Code CLI (`claude -p ... --output-format stream-json`).
|
|
43
|
+
|
|
44
|
+
The behaviour lives in the mixins above; this class only carries the static
|
|
45
|
+
identity + capability flags and the MRO that composes them (mixins first, so
|
|
46
|
+
their overrides win over the `Provider` defaults).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name = "claude"
|
|
50
|
+
display_name = "Claude Code"
|
|
51
|
+
description = "Anthropic Claude Code CLI — line-protocol driver"
|
|
52
|
+
# Same engine as claude-sdk but the plainer driver (no live controls, no
|
|
53
|
+
# ask-mode cards) — kept out of the picker by default so Claude appears
|
|
54
|
+
# once. Flip it on in Settings → Engines.
|
|
55
|
+
default_enabled = False
|
|
56
|
+
capabilities = Capabilities(
|
|
57
|
+
resume=True,
|
|
58
|
+
fork=True,
|
|
59
|
+
permission_modes=True,
|
|
60
|
+
effort=True,
|
|
61
|
+
thinking_display=True,
|
|
62
|
+
context_command=True,
|
|
63
|
+
cumulative_cost=True,
|
|
64
|
+
rich_commit_summaries=True,
|
|
65
|
+
persistent_process=True, # one `claude -p` process serves many turns
|
|
66
|
+
forward_plain_stderr=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Instances this module contributes to the registry. The package scanner
|
|
71
|
+
# (providers/__init__.py) collects every module's PROVIDERS list, so adding a
|
|
72
|
+
# provider is "drop a file that exports PROVIDERS" — no core edit. (A subpackage
|
|
73
|
+
# is discovered exactly like a flat module.)
|
|
74
|
+
PROVIDERS = [ClaudeProvider()]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Claude provider — declarative surface + customization roots.
|
|
2
|
+
|
|
3
|
+
What the Claude adapter *declares about itself*: binary location, accounts,
|
|
4
|
+
models, effort/permission vocabularies, context fetch, and where its
|
|
5
|
+
skills/agents/plugins live. The `ClaudeProvider` combiner in this package's
|
|
6
|
+
``__init__`` mixes this in.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from painapple_code.providers.base import PluginBackend
|
|
15
|
+
from painapple_code.utils.agent_cli import get_claude_binary, fetch_context_tokens
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _CapabilitiesMixin:
|
|
19
|
+
"""Provider self-description: binary, accounts, vocabularies, roots, context."""
|
|
20
|
+
|
|
21
|
+
# --- binary -----------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
# Settings "CLI path" row + generic engine-path endpoint (shared by both
|
|
24
|
+
# Claude drivers — same binary). The app also owns this engine's model
|
|
25
|
+
# catalog (models.yaml), so Settings renders it editable; per-model
|
|
26
|
+
# show/hide prefs are namespaced under the shared "claude" key so both
|
|
27
|
+
# drivers see the same curation.
|
|
28
|
+
path_config_key = "claude_path"
|
|
29
|
+
default_binary = "claude"
|
|
30
|
+
# "Continue in CLI" quick action → `claude -r <session-id>`.
|
|
31
|
+
cli_resume_template = "claude -r {id}"
|
|
32
|
+
models_editable = True
|
|
33
|
+
models_key = "claude"
|
|
34
|
+
# `claude auth status --json` → {loggedIn, authMethod, email,
|
|
35
|
+
# subscriptionType}; the interactive login flow prints an OAuth URL +
|
|
36
|
+
# paste-back code, which works fine inside a PTY terminal tab.
|
|
37
|
+
auth_status_args = ["auth", "status", "--json"]
|
|
38
|
+
auth_login_args = ["auth", "login"]
|
|
39
|
+
|
|
40
|
+
def binary(self) -> str:
|
|
41
|
+
return get_claude_binary()
|
|
42
|
+
|
|
43
|
+
def binary_not_found_hint(self) -> str:
|
|
44
|
+
return ("Make sure the Claude Code CLI is installed and 'claude' is on "
|
|
45
|
+
"the server's PATH, or set the full path in Settings → Engines. "
|
|
46
|
+
"Install with: curl -fsSL https://claude.ai/install.sh | bash")
|
|
47
|
+
|
|
48
|
+
def summary_model_editable(self) -> bool:
|
|
49
|
+
# Claude's journal model lives in models.yaml (`summary_model`), not
|
|
50
|
+
# a global-config key — override storage, keep the knob.
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def get_summary_model_override(self) -> Optional[str]:
|
|
54
|
+
from painapple_code.paths import get_summary_model
|
|
55
|
+
return get_summary_model()
|
|
56
|
+
|
|
57
|
+
def set_summary_model_override(self, value: Optional[str]) -> None:
|
|
58
|
+
# Empty resets to the shipped default — Claude's journal fork always
|
|
59
|
+
# needs a concrete model (`--model` is mandatory on the fork argv).
|
|
60
|
+
from painapple_code import paths
|
|
61
|
+
cleaned = (value or "").strip() or (
|
|
62
|
+
paths.get_default_models_config().get("summary_model")
|
|
63
|
+
or "claude-haiku-4-5")
|
|
64
|
+
paths.save_models_config({
|
|
65
|
+
"selectable": paths.get_selectable_models(),
|
|
66
|
+
"summary_model": cleaned,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
def parse_auth_status(self, returncode: int, stdout: str, stderr: str) -> dict:
|
|
70
|
+
"""Structured status: surface who's logged in and on which plan."""
|
|
71
|
+
try:
|
|
72
|
+
data = json.loads(stdout or "")
|
|
73
|
+
except (ValueError, TypeError):
|
|
74
|
+
data = None
|
|
75
|
+
if not isinstance(data, dict) or "loggedIn" not in data:
|
|
76
|
+
return super().parse_auth_status(returncode, stdout, stderr)
|
|
77
|
+
if not data.get("loggedIn"):
|
|
78
|
+
return {"logged_in": False, "detail": ""}
|
|
79
|
+
bits = [str(b) for b in (data.get("email"), data.get("subscriptionType")) if b]
|
|
80
|
+
return {"logged_in": True, "detail": " · ".join(bits)}
|
|
81
|
+
|
|
82
|
+
# --- accounts / models / effort / permissions -------------------------
|
|
83
|
+
|
|
84
|
+
def accounts(self) -> list[dict]:
|
|
85
|
+
"""Claude accounts == token profiles (`CLAUDE_CODE_OAUTH_TOKEN`).
|
|
86
|
+
|
|
87
|
+
Empty id = the ambient login / global-default token. Each named profile
|
|
88
|
+
is a file under ~/.config/painapple-code/tokens/.
|
|
89
|
+
"""
|
|
90
|
+
from painapple_code.utils.token_profiles import list_profiles
|
|
91
|
+
accounts = [{"id": "", "label": "Default"}]
|
|
92
|
+
accounts += [{"id": p["name"], "label": p["name"]} for p in list_profiles()]
|
|
93
|
+
return accounts
|
|
94
|
+
|
|
95
|
+
def models(self) -> list[dict]:
|
|
96
|
+
from painapple_code.paths import get_selectable_models
|
|
97
|
+
return get_selectable_models()
|
|
98
|
+
|
|
99
|
+
def effort_levels(self) -> list[str]:
|
|
100
|
+
# The Claude CLI's full --effort scale.
|
|
101
|
+
return ["low", "medium", "high", "xhigh", "max"]
|
|
102
|
+
|
|
103
|
+
def permission_modes(self) -> list[dict]:
|
|
104
|
+
# Claude's native permission modes, in display order — fully self-described
|
|
105
|
+
# here (value/label/desc/color), exactly like every other provider. No
|
|
106
|
+
# Claude permission vocabulary lives anywhere else (strings.yaml, a
|
|
107
|
+
# constant, the frontend); the engine picker and chip read this list.
|
|
108
|
+
return [
|
|
109
|
+
{"value": "plan", "label": "Plan",
|
|
110
|
+
"desc": "Read-only, explore & design", "color": "#8b5cf6"},
|
|
111
|
+
{"value": "dontAsk", "label": "Don't Ask",
|
|
112
|
+
"desc": "Auto-deny unless in allow rules", "color": "#facc15"},
|
|
113
|
+
{"value": "acceptEdits", "label": "Accept Edits",
|
|
114
|
+
"desc": "Auto-approve edits in workspace; deny others", "color": "#fb923c"},
|
|
115
|
+
{"value": "auto", "label": "Auto",
|
|
116
|
+
"desc": "AI classifier reviews each tool call", "color": "#60a5fa"},
|
|
117
|
+
{"value": "bypassPermissions", "label": "YOLO",
|
|
118
|
+
"desc": "Skip all permission checks", "color": "#4ade80"},
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def default_permission_mode(self) -> str:
|
|
122
|
+
return "dontAsk"
|
|
123
|
+
|
|
124
|
+
# --- context metering -------------------------------------------------
|
|
125
|
+
|
|
126
|
+
async def fetch_context(
|
|
127
|
+
self,
|
|
128
|
+
cwd: str,
|
|
129
|
+
session_id: Optional[str] = None,
|
|
130
|
+
token_profile: Optional[str] = None,
|
|
131
|
+
model: Optional[str] = None,
|
|
132
|
+
) -> Optional[dict]:
|
|
133
|
+
return await fetch_context_tokens(
|
|
134
|
+
cwd,
|
|
135
|
+
provider_session_id=session_id,
|
|
136
|
+
token_profile=token_profile,
|
|
137
|
+
model=model,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# --- customization roots (skills / agents / plugins) ------------------
|
|
141
|
+
|
|
142
|
+
def skill_roots(self, cwd: str) -> list[dict]:
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
return [
|
|
145
|
+
{"scope": "project", "dir": Path(cwd) / ".claude" / "skills"},
|
|
146
|
+
{"scope": "personal", "dir": Path.home() / ".claude" / "skills"},
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
def agent_roots(self, cwd: str) -> list[dict]:
|
|
150
|
+
from pathlib import Path
|
|
151
|
+
return [
|
|
152
|
+
{"scope": "project", "dir": Path(cwd) / ".claude" / "agents",
|
|
153
|
+
"fmt": "markdown", "writable": True},
|
|
154
|
+
{"scope": "personal", "dir": Path.home() / ".claude" / "agents",
|
|
155
|
+
"fmt": "markdown", "writable": True},
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def plugin_backend(self) -> PluginBackend:
|
|
159
|
+
from pathlib import Path
|
|
160
|
+
return PluginBackend(
|
|
161
|
+
binary=self.binary(),
|
|
162
|
+
list_installed_args=["plugins", "list", "--json"],
|
|
163
|
+
list_all_args=["plugins", "list", "--available", "--json"],
|
|
164
|
+
install_verb=["plugins", "install"],
|
|
165
|
+
uninstall_verb=["plugins", "uninstall"],
|
|
166
|
+
enable_verb=["plugins", "enable"],
|
|
167
|
+
disable_verb=["plugins", "disable"],
|
|
168
|
+
marketplace_root=Path.home() / ".claude" / "plugins" / "marketplaces",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def plugin_skill_dirs(self) -> list[dict]:
|
|
172
|
+
"""Skill roots from installed Claude plugins (read-only).
|
|
173
|
+
|
|
174
|
+
Claude Code caches every known marketplace under
|
|
175
|
+
``~/.claude/plugins/marketplaces/`` but only a subset is installed;
|
|
176
|
+
``installed_plugins.json`` is the authoritative list (keyed
|
|
177
|
+
``<plugin>@<marketplace>``). We surface only installed plugins' skills.
|
|
178
|
+
"""
|
|
179
|
+
from pathlib import Path
|
|
180
|
+
home = Path.home()
|
|
181
|
+
plugins_root = home / ".claude" / "plugins" / "marketplaces"
|
|
182
|
+
if not plugins_root.is_dir():
|
|
183
|
+
return []
|
|
184
|
+
installed = _load_installed_plugin_keys()
|
|
185
|
+
out: list[dict] = []
|
|
186
|
+
for marketplace in sorted(plugins_root.iterdir()):
|
|
187
|
+
plugins_dir = marketplace / "plugins"
|
|
188
|
+
if not plugins_dir.is_dir():
|
|
189
|
+
continue
|
|
190
|
+
for plugin in sorted(plugins_dir.iterdir()):
|
|
191
|
+
if f"{plugin.name}@{marketplace.name}" not in installed:
|
|
192
|
+
continue
|
|
193
|
+
skills_root = plugin / "skills"
|
|
194
|
+
if skills_root.is_dir():
|
|
195
|
+
out.append({
|
|
196
|
+
"label": f"{plugin.name} ({marketplace.name})",
|
|
197
|
+
"dir": skills_root,
|
|
198
|
+
})
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _load_installed_plugin_keys() -> set:
|
|
203
|
+
"""`{plugin@marketplace}` keys from ``~/.claude/plugins/installed_plugins.json``.
|
|
204
|
+
|
|
205
|
+
Empty set if missing/malformed (treat as "nothing installed" rather than
|
|
206
|
+
"show everything", so the picker reflects the user's /plugin install state).
|
|
207
|
+
"""
|
|
208
|
+
from pathlib import Path
|
|
209
|
+
path = Path.home() / ".claude" / "plugins" / "installed_plugins.json"
|
|
210
|
+
try:
|
|
211
|
+
with path.open(encoding="utf-8") as f:
|
|
212
|
+
data = json.load(f)
|
|
213
|
+
except (OSError, json.JSONDecodeError):
|
|
214
|
+
return set()
|
|
215
|
+
plugins = data.get("plugins")
|
|
216
|
+
if not isinstance(plugins, dict):
|
|
217
|
+
return set()
|
|
218
|
+
return set(plugins.keys())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Claude provider — stderr classification and retry/termination policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from painapple_code.providers.base import StderrClass
|
|
8
|
+
from painapple_code.utils.agent_cli import NORMAL_TERMINATION_CODES
|
|
9
|
+
|
|
10
|
+
# Patterns that indicate retryable transient API errors (5xx, overloaded, etc.)
|
|
11
|
+
_RETRYABLE_ERROR_PATTERNS = [
|
|
12
|
+
re.compile(r'API Error:\s*5\d{2}\b'),
|
|
13
|
+
re.compile(r'"type":\s*"api_error"'),
|
|
14
|
+
re.compile(r'overloaded', re.IGNORECASE),
|
|
15
|
+
re.compile(r'Internal server error', re.IGNORECASE),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# Usage/rate/session limit. The CLI wording varies by limit kind: "hit your
|
|
19
|
+
# limit", "hit your session limit", "hit your weekly limit", … — one optional
|
|
20
|
+
# word covers them all without matching unrelated "limit" mentions.
|
|
21
|
+
_USAGE_LIMIT_RE = re.compile(r"hit your (?:\w+\s+)?limit", re.IGNORECASE)
|
|
22
|
+
|
|
23
|
+
# Patterns that indicate NON-retryable errors (checked first to avoid retrying)
|
|
24
|
+
_NON_RETRYABLE_PATTERNS = [
|
|
25
|
+
_USAGE_LIMIT_RE,
|
|
26
|
+
re.compile(r'authentication', re.IGNORECASE),
|
|
27
|
+
re.compile(r'invalid.*api.*key', re.IGNORECASE),
|
|
28
|
+
re.compile(r'credit', re.IGNORECASE),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Patterns in a failed turn's result text that mean "re-login fixes this".
|
|
32
|
+
# Narrower than _NON_RETRYABLE_PATTERNS (no `credit` — a billing problem, not
|
|
33
|
+
# something `claude auth login` resolves).
|
|
34
|
+
_AUTH_ERROR_PATTERNS = [
|
|
35
|
+
re.compile(r'failed to authenticate', re.IGNORECASE),
|
|
36
|
+
re.compile(r'authentication_error', re.IGNORECASE),
|
|
37
|
+
re.compile(r'invalid authentication', re.IGNORECASE),
|
|
38
|
+
re.compile(r'oauth token (?:has )?expired', re.IGNORECASE),
|
|
39
|
+
re.compile(r'invalid.*api.*key', re.IGNORECASE),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _ErrorsMixin:
|
|
44
|
+
"""Maps Claude stderr/result text to the canonical error classes."""
|
|
45
|
+
|
|
46
|
+
def classify_stderr(self, line: str) -> StderrClass:
|
|
47
|
+
if "No conversation found with session ID" in line:
|
|
48
|
+
return StderrClass.STALE_SESSION
|
|
49
|
+
if "compacting" in line.lower():
|
|
50
|
+
return StderrClass.COMPACTING
|
|
51
|
+
if self.is_retryable_api_error(line):
|
|
52
|
+
return StderrClass.RETRYABLE
|
|
53
|
+
return StderrClass.NONE
|
|
54
|
+
|
|
55
|
+
def is_retryable_api_error(self, text: str) -> bool:
|
|
56
|
+
for pattern in _NON_RETRYABLE_PATTERNS:
|
|
57
|
+
if pattern.search(text):
|
|
58
|
+
return False
|
|
59
|
+
for pattern in _RETRYABLE_ERROR_PATTERNS:
|
|
60
|
+
if pattern.search(text):
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def is_auth_error(self, text: str, api_error_status=None) -> bool:
|
|
65
|
+
if api_error_status == 401:
|
|
66
|
+
return True
|
|
67
|
+
return any(p.search(text) for p in _AUTH_ERROR_PATTERNS)
|
|
68
|
+
|
|
69
|
+
def is_usage_limit(self, text: str) -> bool:
|
|
70
|
+
# Genuine limit messages are one-liners (~50-85 chars: "Error during
|
|
71
|
+
# compaction: You've hit your session limit · resets 4pm (…)"). The
|
|
72
|
+
# length cap keeps the substring match from misfiring on long text
|
|
73
|
+
# that merely QUOTES the phrase — an assistant reply discussing this
|
|
74
|
+
# very bug, pasted logs inside an error string, etc.
|
|
75
|
+
return bool(text) and len(text) <= 200 and bool(_USAGE_LIMIT_RE.search(text))
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def normal_termination_codes(self) -> frozenset:
|
|
79
|
+
return NORMAL_TERMINATION_CODES
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Claude provider — command building.
|
|
2
|
+
|
|
3
|
+
Turns a `LaunchOptions` into the persistent `claude -p … --output-format
|
|
4
|
+
stream-json` argv (model, effort, permission mode, resume/fork). Mixed into
|
|
5
|
+
`ClaudeProvider`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from painapple_code.providers.base import LaunchOptions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _LaunchMixin:
|
|
14
|
+
"""Builds the `claude -p` command line."""
|
|
15
|
+
|
|
16
|
+
def build_command(self, opts: LaunchOptions) -> list[str]:
|
|
17
|
+
cmd = [
|
|
18
|
+
self.binary(),
|
|
19
|
+
"-p",
|
|
20
|
+
"--input-format", "stream-json",
|
|
21
|
+
"--output-format", "stream-json",
|
|
22
|
+
"--verbose",
|
|
23
|
+
# Opus 4.7 changed the thinking-block default from "summarized"
|
|
24
|
+
# to "omitted" (empty text, signature only). Restore summaries
|
|
25
|
+
# on all models — the flag is a no-op where display was already
|
|
26
|
+
# "summarized". See anthropics/claude-code#8477 comment 4269024077.
|
|
27
|
+
"--thinking-display", "summarized",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
if opts.model:
|
|
31
|
+
cmd.extend(["--model", opts.model])
|
|
32
|
+
|
|
33
|
+
if opts.fallback_model:
|
|
34
|
+
cmd.extend(["--fallback-model", opts.fallback_model])
|
|
35
|
+
|
|
36
|
+
# "high" is the CLI default; skip to avoid noise.
|
|
37
|
+
if opts.effort and opts.effort != "high":
|
|
38
|
+
cmd.extend(["--effort", opts.effort])
|
|
39
|
+
|
|
40
|
+
# Fall back to this provider's own default when no mode was resolved.
|
|
41
|
+
cmd.extend(["--permission-mode", opts.permission_mode or self.default_permission_mode()])
|
|
42
|
+
|
|
43
|
+
# Fork from another session (new session with conversation history),
|
|
44
|
+
# else resume an existing one.
|
|
45
|
+
if opts.fork_from_session_id:
|
|
46
|
+
cmd.extend(["--resume", opts.fork_from_session_id, "--fork-session"])
|
|
47
|
+
elif opts.session_id:
|
|
48
|
+
cmd.extend(["--resume", opts.session_id])
|
|
49
|
+
|
|
50
|
+
return cmd
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Claude provider — the rich-commit summary fork.
|
|
2
|
+
|
|
3
|
+
Forks the live `claude -p` session with Haiku to write structured commit
|
|
4
|
+
sections, then pulls `structured_output` + usage out of the wrapper JSON. Mixed
|
|
5
|
+
into `ClaudeProvider`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from painapple_code.providers.base import SummaryForkPlan
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _SummaryMixin:
|
|
17
|
+
"""Fork the live session with Haiku and read back the structured sections."""
|
|
18
|
+
|
|
19
|
+
def build_summary_fork(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
session_id: str,
|
|
23
|
+
fork_prompt: str,
|
|
24
|
+
schema_json: str,
|
|
25
|
+
cwd: str,
|
|
26
|
+
token_profile: Optional[str] = None,
|
|
27
|
+
session_model: Optional[str] = None,
|
|
28
|
+
) -> Optional[SummaryForkPlan]:
|
|
29
|
+
"""Fork the live session with Haiku to write structured commit sections.
|
|
30
|
+
|
|
31
|
+
`--fork-session` branches a throwaway session off the real one, so the
|
|
32
|
+
summary turn never touches the user's conversation; `--json-schema`
|
|
33
|
+
forces the structured shape and `--output-format json` wraps it with
|
|
34
|
+
cost/usage on stdout.
|
|
35
|
+
|
|
36
|
+
The prompt is fed over stdin (bare `-p`, no positional) rather than as an
|
|
37
|
+
argv element: the journey + tools prompt can be large, so this keeps it
|
|
38
|
+
clear of ARG_MAX and out of `ps`. The generic spawner writes
|
|
39
|
+
`stdin_input` before reading stdout.
|
|
40
|
+
"""
|
|
41
|
+
from painapple_code.paths import get_summary_model
|
|
42
|
+
from painapple_code.utils.token_profiles import build_env as build_token_env
|
|
43
|
+
|
|
44
|
+
# Always plain haiku — the [1m] variant hangs with --fork-session from
|
|
45
|
+
# opus[1m] sessions (every fork times out at 300s even for 30k context).
|
|
46
|
+
summary_model = get_summary_model()
|
|
47
|
+
return SummaryForkPlan(
|
|
48
|
+
argv=[
|
|
49
|
+
self.binary(),
|
|
50
|
+
"--resume", session_id,
|
|
51
|
+
"--fork-session",
|
|
52
|
+
"--model", summary_model,
|
|
53
|
+
"--tools", "", # disable all tools
|
|
54
|
+
"--no-session-persistence",
|
|
55
|
+
"-p", # print mode; prompt arrives on stdin
|
|
56
|
+
"--output-format", "json", # wrapper JSON carries cost/usage
|
|
57
|
+
"--json-schema", schema_json, # force structured output
|
|
58
|
+
],
|
|
59
|
+
env=build_token_env(token_profile),
|
|
60
|
+
model=summary_model,
|
|
61
|
+
output_file=None, # structured_output is on stdout
|
|
62
|
+
stdin_input=fork_prompt, # feed the prompt via stdin (not argv)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def parse_summary_fork(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
plan: SummaryForkPlan,
|
|
69
|
+
returncode: int,
|
|
70
|
+
stdout: bytes,
|
|
71
|
+
stderr: bytes,
|
|
72
|
+
) -> tuple[Optional[dict], Optional[dict]]:
|
|
73
|
+
"""Pull `structured_output` + usage out of the wrapper JSON on stdout."""
|
|
74
|
+
if returncode != 0:
|
|
75
|
+
return None, None
|
|
76
|
+
try:
|
|
77
|
+
result = json.loads(stdout.decode())
|
|
78
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
79
|
+
return None, None
|
|
80
|
+
structured = result.get("structured_output")
|
|
81
|
+
if not isinstance(structured, dict):
|
|
82
|
+
return None, None
|
|
83
|
+
usage = result.get("usage") or {}
|
|
84
|
+
cost = {
|
|
85
|
+
"cost": result.get("total_cost_usd", 0.0) or 0.0,
|
|
86
|
+
"input_tokens": usage.get("input_tokens", 0),
|
|
87
|
+
"output_tokens": usage.get("output_tokens", 0),
|
|
88
|
+
}
|
|
89
|
+
return structured, cost
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Claude provider — wire adaptation + output normalization.
|
|
2
|
+
|
|
3
|
+
Claude's stream-json *is* the canonical internal event shape, so the wire
|
|
4
|
+
methods here (`frame_input`, `session_id_from_event`) are the identity readings
|
|
5
|
+
of that schema, and the base `translate_events` identity is inherited unchanged.
|
|
6
|
+
The one real transform Claude needs is `normalize_result`: the CLI's `result`
|
|
7
|
+
cost/usage fields are cumulative across the persistent `claude -p` process, and
|
|
8
|
+
the server stores them per-turn. Mixed into `ClaudeProvider`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from painapple_code.providers.base import CostState
|
|
16
|
+
|
|
17
|
+
# Cumulative `modelUsage` fields tracked for the per-turn delta conversion.
|
|
18
|
+
_CUM_USAGE_FIELDS = (
|
|
19
|
+
"costUSD", "inputTokens", "outputTokens",
|
|
20
|
+
"cacheReadInputTokens", "cacheCreationInputTokens",
|
|
21
|
+
"webSearchRequests",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _TranslateMixin:
|
|
26
|
+
"""Identity wire methods + per-turn delta conversion for the cumulative result."""
|
|
27
|
+
|
|
28
|
+
def frame_input(self, message: dict) -> dict:
|
|
29
|
+
# Identity: the canonical user-message envelope is exactly what the
|
|
30
|
+
# `claude -p --input-format stream-json` stdin protocol expects
|
|
31
|
+
# ({"type": "user", "message": {...}}).
|
|
32
|
+
return message
|
|
33
|
+
|
|
34
|
+
def session_id_from_event(self, event: dict) -> Optional[str]:
|
|
35
|
+
# Present on every stream-json line the CLI emits.
|
|
36
|
+
return event.get("session_id")
|
|
37
|
+
|
|
38
|
+
def normalize_result(self, msg: dict, state: CostState) -> None:
|
|
39
|
+
"""Convert CLI `result` fields from per-process cumulative to per-turn delta.
|
|
40
|
+
|
|
41
|
+
The Claude CLI emits `result` messages whose `total_cost_usd` and
|
|
42
|
+
`modelUsage[*]` fields are running totals across the lifetime of the
|
|
43
|
+
subprocess (across multiple user prompts within one `claude -p`
|
|
44
|
+
invocation). Painapple stores these per-turn, so summing them
|
|
45
|
+
double-counts. This rewrites `msg` in place: cumulative values become
|
|
46
|
+
per-turn deltas (subtracting the previous cumulative seen on this
|
|
47
|
+
subprocess, tracked in `state`).
|
|
48
|
+
|
|
49
|
+
Resilient to out-of-order or negative deltas: if a value somehow drops
|
|
50
|
+
(shouldn't happen within a process, but defensive), treat it as a fresh
|
|
51
|
+
count from zero.
|
|
52
|
+
"""
|
|
53
|
+
cum_cost = msg.get("total_cost_usd", 0) or 0.0
|
|
54
|
+
delta_cost = cum_cost - state.cum_cost
|
|
55
|
+
if delta_cost < 0:
|
|
56
|
+
delta_cost = cum_cost
|
|
57
|
+
msg["total_cost_usd"] = delta_cost
|
|
58
|
+
state.cum_cost = cum_cost
|
|
59
|
+
|
|
60
|
+
mu = msg.get("modelUsage") or {}
|
|
61
|
+
if not isinstance(mu, dict):
|
|
62
|
+
return
|
|
63
|
+
prev = state.cum_usage or {}
|
|
64
|
+
new_prev = {}
|
|
65
|
+
for model, info in list(mu.items()):
|
|
66
|
+
if not isinstance(info, dict):
|
|
67
|
+
continue
|
|
68
|
+
prev_info = prev.get(model, {})
|
|
69
|
+
new_prev[model] = {f: info.get(f, 0) or 0 for f in _CUM_USAGE_FIELDS}
|
|
70
|
+
for f in _CUM_USAGE_FIELDS:
|
|
71
|
+
cur = info.get(f, 0) or 0
|
|
72
|
+
d = cur - (prev_info.get(f, 0) or 0)
|
|
73
|
+
info[f] = d if d >= 0 else cur
|
|
74
|
+
state.cum_usage = new_prev
|