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,321 @@
|
|
|
1
|
+
"""Codex provider — declarative surface + customization roots.
|
|
2
|
+
|
|
3
|
+
What the Codex adapter *declares about itself*: binary location, availability,
|
|
4
|
+
effort/permission vocabularies, and where its skills/agents/plugins live. The
|
|
5
|
+
`CodexProvider` combiner in this package's ``__init__`` mixes this in. Kept
|
|
6
|
+
separate from launch/translate so the provider's static description reads in one
|
|
7
|
+
place.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from painapple_code import paths
|
|
16
|
+
from painapple_code.providers.base import PluginBackend
|
|
17
|
+
|
|
18
|
+
# Context-window size used to meter the context bar. `codex exec --json` reports
|
|
19
|
+
# per-turn token usage but — unlike the TUI / app-server protocol — emits NO
|
|
20
|
+
# window size and has no `/context` command, so we supply it. The gpt-5-codex
|
|
21
|
+
# family is metered by Codex against a 272K-token *input* window (the input split
|
|
22
|
+
# of the 400K product window; Codex then applies a ~95% effective cap on top —
|
|
23
|
+
# see openai/codex#19319, #19409). The app doesn't manage which Codex model is
|
|
24
|
+
# active (models() is empty — it's chosen by `codex login` / config.toml), so we
|
|
25
|
+
# meter against this family default rather than a per-model lookup. When a real
|
|
26
|
+
# Codex model catalog with per-model windows is registered, context_from_result
|
|
27
|
+
# can switch to a model-keyed size with no other change.
|
|
28
|
+
_DEFAULT_CONTEXT_WINDOW = 272_000
|
|
29
|
+
|
|
30
|
+
# Canonical ReasoningEffort ordering — used to order the union vocabulary and
|
|
31
|
+
# to clamp a requested level down to the nearest one a model supports. The
|
|
32
|
+
# per-model truth is the CLI's own models_cache (supported_reasoning_levels).
|
|
33
|
+
_EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]
|
|
34
|
+
# Pre-cache fallback (codex installed but never run): the classic triad every
|
|
35
|
+
# codex model speaks.
|
|
36
|
+
_FALLBACK_EFFORTS = ["low", "medium", "high"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _CapabilitiesMixin:
|
|
40
|
+
"""Provider self-description: binary, availability, vocabularies, roots."""
|
|
41
|
+
|
|
42
|
+
# --- binary / availability -------------------------------------------
|
|
43
|
+
|
|
44
|
+
# Settings "CLI path" row + generic engine-path endpoint (shared by both
|
|
45
|
+
# Codex drivers — same binary). Model definitions stay read-only in
|
|
46
|
+
# Settings — the Codex CLI owns them (models_cache.json) — but per-model
|
|
47
|
+
# show/hide toggles apply, namespaced under the shared "codex" key so
|
|
48
|
+
# both drivers see the same curation.
|
|
49
|
+
path_config_key = "codex_path"
|
|
50
|
+
default_binary = "codex"
|
|
51
|
+
# "Continue in CLI" quick action → `codex exec resume <thread-id>` (both
|
|
52
|
+
# Codex drivers resume the same rollout by thread id; codex_app_server
|
|
53
|
+
# inherits this mixin).
|
|
54
|
+
cli_resume_template = "codex exec resume {id}"
|
|
55
|
+
models_key = "codex"
|
|
56
|
+
# `codex login status` exits 0/1 with a one-line status (base parser
|
|
57
|
+
# handles it — note codex prints to stderr). Login uses the device-code
|
|
58
|
+
# flow: plain `codex login` binds a localhost callback on the SERVER,
|
|
59
|
+
# which a remote client can never reach; --device-auth prints a URL +
|
|
60
|
+
# code that work from any device. (Caution: starting either flow drops
|
|
61
|
+
# the existing auth.json before the new login completes.)
|
|
62
|
+
auth_status_args = ["login", "status"]
|
|
63
|
+
auth_login_args = ["login", "--device-auth"]
|
|
64
|
+
# Auto-journal: both Codex drivers self-summarize on the Codex side and
|
|
65
|
+
# share the optional cheaper-summarizer override; empty = the summary
|
|
66
|
+
# fork inherits the session thread's model.
|
|
67
|
+
summary_model_config_key = "codex_summary_model"
|
|
68
|
+
summary_model_placeholder = "session model"
|
|
69
|
+
|
|
70
|
+
def parse_auth_status(self, returncode: int, stdout: str, stderr: str) -> dict:
|
|
71
|
+
parsed = super().parse_auth_status(returncode, stdout, stderr)
|
|
72
|
+
if not parsed.get("logged_in"):
|
|
73
|
+
# Codex's logged-out line is literally "Not logged in" — the UI
|
|
74
|
+
# already says that; an echoed detail would just duplicate it.
|
|
75
|
+
parsed["detail"] = ""
|
|
76
|
+
return parsed
|
|
77
|
+
|
|
78
|
+
def binary(self) -> str:
|
|
79
|
+
return paths.load_global_config().get(
|
|
80
|
+
self.path_config_key, self.default_binary)
|
|
81
|
+
|
|
82
|
+
def is_available(self) -> tuple[bool, Optional[str]]:
|
|
83
|
+
"""Codex needs both the CLI on PATH and an authenticated `codex login`.
|
|
84
|
+
|
|
85
|
+
Auth lives in `$CODEX_HOME/auth.json` (or `~/.codex/`); an API key in the
|
|
86
|
+
environment also works. We only block when there's *no* sign of setup —
|
|
87
|
+
a present config dir is treated as good enough so we don't false-negative.
|
|
88
|
+
"""
|
|
89
|
+
ok, reason = super().is_available()
|
|
90
|
+
if not ok:
|
|
91
|
+
return False, "Codex CLI not installed — `npm i -g @openai/codex`"
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
codex_home = Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
|
|
94
|
+
authed = (
|
|
95
|
+
(codex_home / "auth.json").exists()
|
|
96
|
+
or (codex_home / "config.toml").exists()
|
|
97
|
+
or codex_home.is_dir()
|
|
98
|
+
or bool(os.environ.get("CODEX_API_KEY") or os.environ.get("OPENAI_API_KEY"))
|
|
99
|
+
)
|
|
100
|
+
if not authed:
|
|
101
|
+
return False, "Codex not signed in — run `codex login`"
|
|
102
|
+
return True, None
|
|
103
|
+
|
|
104
|
+
def _listed_models_cache(self) -> list[dict]:
|
|
105
|
+
"""Raw ``visibility=list`` entries from the CLI's models cache,
|
|
106
|
+
priority-sorted. Shared source for models() / effort_levels() /
|
|
107
|
+
effort_for_model(). Missing/malformed cache → empty list."""
|
|
108
|
+
import json
|
|
109
|
+
from pathlib import Path
|
|
110
|
+
codex_home = Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
|
|
111
|
+
try:
|
|
112
|
+
with (codex_home / "models_cache.json").open(encoding="utf-8") as f:
|
|
113
|
+
data = json.load(f)
|
|
114
|
+
except (OSError, json.JSONDecodeError):
|
|
115
|
+
return []
|
|
116
|
+
entries = data.get("models") if isinstance(data, dict) else None
|
|
117
|
+
if not isinstance(entries, list):
|
|
118
|
+
return []
|
|
119
|
+
listed = [
|
|
120
|
+
m for m in entries
|
|
121
|
+
if isinstance(m, dict) and m.get("visibility") == "list" and m.get("slug")
|
|
122
|
+
]
|
|
123
|
+
listed.sort(key=lambda m: m.get("priority") or 0)
|
|
124
|
+
return listed
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _model_efforts(entry: dict) -> list[str]:
|
|
128
|
+
"""One cache entry's ``supported_reasoning_levels`` → effort names."""
|
|
129
|
+
out: list[str] = []
|
|
130
|
+
for lvl in entry.get("supported_reasoning_levels") or []:
|
|
131
|
+
effort = lvl.get("effort") if isinstance(lvl, dict) else None
|
|
132
|
+
if isinstance(effort, str) and effort:
|
|
133
|
+
out.append(effort)
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
def models(self) -> list[dict]:
|
|
137
|
+
"""Codex's own model catalog, read from the CLI's cache.
|
|
138
|
+
|
|
139
|
+
The Codex CLI maintains ``$CODEX_HOME/models_cache.json`` (refreshed
|
|
140
|
+
from the backend's model registry on every run): slug, display name,
|
|
141
|
+
description, visibility, priority and supported reasoning levels per
|
|
142
|
+
model. Surfacing that file — rather than a hardcoded list — keeps the
|
|
143
|
+
picker in lockstep with whatever this Codex install actually offers.
|
|
144
|
+
Missing/malformed cache (codex installed but never run) → empty
|
|
145
|
+
catalog, and the UI shows only the engine-default option.
|
|
146
|
+
|
|
147
|
+
The launch paths guard on this: exec passes ``-m`` only for ids listed
|
|
148
|
+
here; the app-server forwards any non-Claude id (so an explicit pick
|
|
149
|
+
still works even while the cache is absent). With a catalog present the
|
|
150
|
+
app OWNS the default — `paths.engine_default_model` resolves the
|
|
151
|
+
top (priority-first) listed model and the launch forwards it, so a
|
|
152
|
+
fresh session runs a concrete model rather than deferring to the CLI's
|
|
153
|
+
own config. Only an ABSENT catalog (no cache) falls back to
|
|
154
|
+
`codex login` / config.toml (and the picker/chip hide entirely).
|
|
155
|
+
|
|
156
|
+
Each entry carries ``efforts`` — the model's OWN reasoning-effort
|
|
157
|
+
range — so pickers can narrow the engine-level union to the selected
|
|
158
|
+
model (launch clamps to the same list via effort_for_model).
|
|
159
|
+
"""
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
"id": m["slug"],
|
|
163
|
+
"label": m.get("display_name") or m["slug"],
|
|
164
|
+
"desc": m.get("description") or "",
|
|
165
|
+
"efforts": [
|
|
166
|
+
lvl for lvl in _EFFORT_ORDER
|
|
167
|
+
if lvl in self._model_efforts(m)
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
for m in self._listed_models_cache()
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# --- effort / permissions --------------------------------------------
|
|
174
|
+
|
|
175
|
+
def effort_levels(self) -> list[str]:
|
|
176
|
+
"""Codex's effort vocabulary, read from the CLI's own model registry.
|
|
177
|
+
|
|
178
|
+
``models_cache.json`` self-describes ``supported_reasoning_levels``
|
|
179
|
+
per model (the 5.6 family reaches xhigh/max/ultra; older models stop
|
|
180
|
+
at xhigh). The engine-level vocabulary is the ordered union across
|
|
181
|
+
the listed catalog — the offering is wide, and ``effort_for_model``
|
|
182
|
+
clamps at launch to what the *target* model actually supports.
|
|
183
|
+
Missing cache → the classic triad every codex model speaks.
|
|
184
|
+
"""
|
|
185
|
+
union: set = set()
|
|
186
|
+
for entry in self._listed_models_cache():
|
|
187
|
+
union.update(self._model_efforts(entry))
|
|
188
|
+
ordered = [lvl for lvl in _EFFORT_ORDER if lvl in union]
|
|
189
|
+
return ordered or list(_FALLBACK_EFFORTS)
|
|
190
|
+
|
|
191
|
+
def effort_for_model(self, effort: Optional[str], model: Optional[str]) -> Optional[str]:
|
|
192
|
+
"""Clamp a requested reasoning effort to the target model's own range.
|
|
193
|
+
|
|
194
|
+
The wire value must be one the model actually supports — the CLI's
|
|
195
|
+
registry is authoritative, and an out-of-range value risks a rejected
|
|
196
|
+
turn. Known model → its ``supported_reasoning_levels``; unknown or
|
|
197
|
+
default model → the intersection every listed model speaks (still
|
|
198
|
+
cache-driven); no cache → the classic triad. A level above the range
|
|
199
|
+
clamps down to the highest supported one (``ultra`` → ``max`` on a
|
|
200
|
+
model that stops there; legacy Claude ``max`` → ``xhigh`` where that's
|
|
201
|
+
the top); one below clamps up to the lowest. Returns None when no
|
|
202
|
+
effort was requested (or the value is garbage) — codex then runs its
|
|
203
|
+
own default.
|
|
204
|
+
"""
|
|
205
|
+
if not effort or effort not in _EFFORT_ORDER:
|
|
206
|
+
return None
|
|
207
|
+
entries = self._listed_models_cache()
|
|
208
|
+
supported: list[str] = []
|
|
209
|
+
if entries:
|
|
210
|
+
if model:
|
|
211
|
+
entry = next((m for m in entries if m.get("slug") == model), None)
|
|
212
|
+
if entry:
|
|
213
|
+
supported = [
|
|
214
|
+
lvl for lvl in _EFFORT_ORDER
|
|
215
|
+
if lvl in self._model_efforts(entry)
|
|
216
|
+
]
|
|
217
|
+
if not supported:
|
|
218
|
+
sets = [s for s in (set(self._model_efforts(m)) for m in entries) if s]
|
|
219
|
+
common = set.intersection(*sets) if sets else set()
|
|
220
|
+
supported = [lvl for lvl in _EFFORT_ORDER if lvl in common]
|
|
221
|
+
if not supported:
|
|
222
|
+
supported = list(_FALLBACK_EFFORTS)
|
|
223
|
+
if effort in supported:
|
|
224
|
+
return effort
|
|
225
|
+
idx = _EFFORT_ORDER.index(effort)
|
|
226
|
+
below = [lvl for lvl in supported if _EFFORT_ORDER.index(lvl) <= idx]
|
|
227
|
+
return below[-1] if below else supported[0]
|
|
228
|
+
|
|
229
|
+
def permission_modes(self) -> list[dict]:
|
|
230
|
+
# Codex's OWN native sandbox vocabulary — the provider owns this list and
|
|
231
|
+
# the session stores these values directly (no Claude-vocabulary
|
|
232
|
+
# indirection). `color` lets the UI render each tier without knowing
|
|
233
|
+
# Codex's scheme. _SANDBOX_BY_MODE (launch.py) passes them to `--sandbox`.
|
|
234
|
+
return [
|
|
235
|
+
{"value": "read-only", "label": "Read-only",
|
|
236
|
+
"desc": "Sandboxed — no file writes", "color": "#8b5cf6"},
|
|
237
|
+
{"value": "workspace-write", "label": "Workspace write",
|
|
238
|
+
"desc": "Edit files in the workspace", "color": "#fb923c"},
|
|
239
|
+
{"value": "danger-full-access", "label": "Full access",
|
|
240
|
+
"desc": "No sandbox — full disk & network", "color": "#f87171"},
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
def default_permission_mode(self) -> str:
|
|
244
|
+
# A fresh Codex session should be able to do work but stay sandboxed —
|
|
245
|
+
# not inherit Claude's global default (e.g. 'dontAsk' → read-only).
|
|
246
|
+
return "workspace-write"
|
|
247
|
+
|
|
248
|
+
# --- customization roots (skills / plugins / agents) ------------------
|
|
249
|
+
|
|
250
|
+
def skill_roots(self, cwd: str) -> list[dict]:
|
|
251
|
+
# Codex uses the same folder-form SKILL.md format as Claude, only the
|
|
252
|
+
# locations differ. `.agents/skills` is the preferred cross-agent
|
|
253
|
+
# convention; `.codex/skills` is the still-supported repo layout; the
|
|
254
|
+
# CODEX_HOME location is deprecated-but-supported for the user scope.
|
|
255
|
+
# Listed highest-priority first — the first project/personal entry is
|
|
256
|
+
# also where a new skill of that scope is created.
|
|
257
|
+
from pathlib import Path
|
|
258
|
+
home = Path.home()
|
|
259
|
+
codex_home = Path(os.environ.get("CODEX_HOME") or home / ".codex")
|
|
260
|
+
return [
|
|
261
|
+
{"scope": "project", "dir": Path(cwd) / ".agents" / "skills"},
|
|
262
|
+
{"scope": "project", "dir": Path(cwd) / ".codex" / "skills"},
|
|
263
|
+
{"scope": "personal", "dir": home / ".agents" / "skills"},
|
|
264
|
+
{"scope": "personal", "dir": codex_home / "skills"},
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
def plugin_backend(self) -> PluginBackend:
|
|
268
|
+
# `codex plugin list --json` → {installed, available} (same shape as
|
|
269
|
+
# Claude); `codex plugin add/remove` install/uninstall. Codex has no
|
|
270
|
+
# enable/disable (the UI hides the toggle). Inventory is skipped: Codex
|
|
271
|
+
# caches plugins under an unstable ~/.codex/.tmp snapshot dir, and bundles
|
|
272
|
+
# components differently — not worth walking here.
|
|
273
|
+
return PluginBackend(
|
|
274
|
+
binary=self.binary(),
|
|
275
|
+
list_installed_args=["plugin", "list", "--json"],
|
|
276
|
+
list_all_args=["plugin", "list", "--json"],
|
|
277
|
+
install_verb=["plugin", "add"],
|
|
278
|
+
uninstall_verb=["plugin", "remove"],
|
|
279
|
+
enable_verb=None,
|
|
280
|
+
disable_verb=None,
|
|
281
|
+
marketplace_root=None,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def agent_roots(self, cwd: str) -> list[dict]:
|
|
285
|
+
# Codex agents are TOML (`<name>.toml`): project `.codex/agents`, user
|
|
286
|
+
# `~/.codex/agents` (or $CODEX_HOME/agents). Read-only in the browser for
|
|
287
|
+
# now — the schema (developer_instructions / model_reasoning_effort /
|
|
288
|
+
# sandbox_mode) and TOML round-trip differ from Claude's markdown agents;
|
|
289
|
+
# editing is a separate follow-up. (Built-in roles default/worker/explorer
|
|
290
|
+
# have no file and aren't listed, mirroring Claude's built-in subagents.)
|
|
291
|
+
from pathlib import Path
|
|
292
|
+
home = Path.home()
|
|
293
|
+
codex_home = Path(os.environ.get("CODEX_HOME") or home / ".codex")
|
|
294
|
+
return [
|
|
295
|
+
{"scope": "project", "dir": Path(cwd) / ".codex" / "agents",
|
|
296
|
+
"fmt": "toml", "writable": False},
|
|
297
|
+
{"scope": "personal", "dir": codex_home / "agents",
|
|
298
|
+
"fmt": "toml", "writable": False},
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
# --- context metering -------------------------------------------------
|
|
302
|
+
|
|
303
|
+
def context_from_result(self, result_msg: dict, model: Optional[str] = None) -> Optional[dict]:
|
|
304
|
+
# Codex has no /context command, but turn.completed.usage.input_tokens is
|
|
305
|
+
# the full conversation context fed to the model that turn — i.e. the live
|
|
306
|
+
# window occupancy, which is exactly what Codex's own TUI meters. The
|
|
307
|
+
# canonical result (built by _result_from_usage) carries it as
|
|
308
|
+
# usage.input_tokens; cached tokens are a *subset* of that, so we don't add
|
|
309
|
+
# them. No per-section breakdown exists in exec mode.
|
|
310
|
+
usage = (result_msg or {}).get("usage") or {}
|
|
311
|
+
in_tok = usage.get("input_tokens") or 0
|
|
312
|
+
if not in_tok:
|
|
313
|
+
return None
|
|
314
|
+
window = _DEFAULT_CONTEXT_WINDOW
|
|
315
|
+
return {
|
|
316
|
+
"contextTokens": in_tok,
|
|
317
|
+
"contextWindow": window,
|
|
318
|
+
"percentage": round(in_tok / window * 100, 1) if window else None,
|
|
319
|
+
"breakdown": None,
|
|
320
|
+
"memoryFiles": None,
|
|
321
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Codex provider — stderr classification and retry/termination policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from painapple_code.providers.base import StderrClass
|
|
6
|
+
|
|
7
|
+
# Retryable transient-error substrings on Codex stderr / result text.
|
|
8
|
+
_RETRYABLE_SUBSTRINGS = (
|
|
9
|
+
"rate limit", "429", "stream error", "503", "502", "500",
|
|
10
|
+
"overloaded", "timed out", "connection reset",
|
|
11
|
+
)
|
|
12
|
+
# Codex resume of a thread that no longer exists.
|
|
13
|
+
_STALE_SUBSTRINGS = ("no rollout found", "session not found", "thread not found")
|
|
14
|
+
|
|
15
|
+
# ChatGPT-plan / API usage-limit one-liners on turn.failed / error events.
|
|
16
|
+
_USAGE_LIMIT_SUBSTRINGS = ("usage limit", "hit your limit", "rate limit reached")
|
|
17
|
+
|
|
18
|
+
# Auth failures the user can fix by re-logging in (`codex login`).
|
|
19
|
+
_AUTH_SUBSTRINGS = (
|
|
20
|
+
"401", "unauthorized", "token expired", "not logged in",
|
|
21
|
+
"login required", "invalid api key", "authentication",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _ErrorsMixin:
|
|
26
|
+
"""Maps Codex stderr/result text to the canonical error classes."""
|
|
27
|
+
|
|
28
|
+
def classify_stderr(self, line: str) -> StderrClass:
|
|
29
|
+
low = line.lower()
|
|
30
|
+
if any(s in low for s in _STALE_SUBSTRINGS):
|
|
31
|
+
return StderrClass.STALE_SESSION
|
|
32
|
+
if any(s in low for s in _RETRYABLE_SUBSTRINGS):
|
|
33
|
+
return StderrClass.RETRYABLE
|
|
34
|
+
return StderrClass.NONE
|
|
35
|
+
|
|
36
|
+
def is_retryable_api_error(self, text: str) -> bool:
|
|
37
|
+
low = text.lower()
|
|
38
|
+
# Auth failures need a re-login, not a resend — without this guard a
|
|
39
|
+
# message containing both ("401 ... stream error") would raise the
|
|
40
|
+
# re-login card AND kick off a doomed server-level retry loop.
|
|
41
|
+
if any(s in low for s in _AUTH_SUBSTRINGS):
|
|
42
|
+
return False
|
|
43
|
+
return any(s in low for s in _RETRYABLE_SUBSTRINGS)
|
|
44
|
+
|
|
45
|
+
def is_usage_limit(self, text: str) -> bool:
|
|
46
|
+
# Genuine limit reports are one-liners; the length gate keeps prose that
|
|
47
|
+
# merely quotes a limit phrase from ever matching (same contract as the
|
|
48
|
+
# Claude provider's matcher).
|
|
49
|
+
if not text or len(text) > 200:
|
|
50
|
+
return False
|
|
51
|
+
low = text.lower()
|
|
52
|
+
return any(s in low for s in _USAGE_LIMIT_SUBSTRINGS)
|
|
53
|
+
|
|
54
|
+
def is_auth_error(self, text: str, api_error_status=None) -> bool:
|
|
55
|
+
if api_error_status == 401:
|
|
56
|
+
return True
|
|
57
|
+
if not text or len(text) > 300:
|
|
58
|
+
return False
|
|
59
|
+
low = text.lower()
|
|
60
|
+
return any(s in low for s in _AUTH_SUBSTRINGS)
|
|
61
|
+
|
|
62
|
+
def binary_not_found_hint(self) -> str:
|
|
63
|
+
return ("Install the Codex CLI (`npm i -g @openai/codex`) or point the "
|
|
64
|
+
"CLI path at it in Settings → Engines (`codex_path` config key).")
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def normal_termination_codes(self) -> frozenset:
|
|
68
|
+
# A finished `codex exec` turn exits 0; SIGINT/SIGTERM stops map to
|
|
69
|
+
# 130/143 like any subprocess.
|
|
70
|
+
return frozenset({0, 130, 143})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Codex provider — command building and image materialization.
|
|
2
|
+
|
|
3
|
+
Turns a `LaunchOptions` into the `codex exec [resume <id>] --json …` argv,
|
|
4
|
+
including the effort→`model_reasoning_effort` and permission→`--sandbox`
|
|
5
|
+
mappings. Mixed into `CodexProvider`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from painapple_code import paths
|
|
13
|
+
from painapple_code.providers.base import LaunchOptions
|
|
14
|
+
|
|
15
|
+
# Effort → `model_reasoning_effort` goes through capabilities'
|
|
16
|
+
# effort_for_model(): the CLI's own models_cache declares each model's
|
|
17
|
+
# supported reasoning levels, and the requested level is clamped to the
|
|
18
|
+
# TARGET model's range (unknown/default model → the range every listed
|
|
19
|
+
# model speaks; no cache → the classic low/medium/high triad).
|
|
20
|
+
|
|
21
|
+
# Permission value → codex `--sandbox` policy. `codex exec` has no approval flag
|
|
22
|
+
# (it runs non-interactively with approvals off); the sandbox policy alone
|
|
23
|
+
# governs what model-generated commands may do. read-only blocks all writes;
|
|
24
|
+
# workspace-write allows in-workspace edits; danger-full-access removes the
|
|
25
|
+
# sandbox entirely.
|
|
26
|
+
#
|
|
27
|
+
# New sessions store Codex's OWN native vocabulary (see permission_modes), so
|
|
28
|
+
# those map to themselves. The Claude-vocabulary rows are a back-compat shim for
|
|
29
|
+
# sessions created before Codex registered native modes — provider is frozen
|
|
30
|
+
# after the first turn, so such a session keeps its stored canonical value.
|
|
31
|
+
_SANDBOX_BY_MODE = {
|
|
32
|
+
# Codex-native (identity)
|
|
33
|
+
"read-only": "read-only",
|
|
34
|
+
"workspace-write": "workspace-write",
|
|
35
|
+
"danger-full-access": "danger-full-access",
|
|
36
|
+
# Legacy Claude-vocabulary (pre-native sessions)
|
|
37
|
+
"plan": "read-only",
|
|
38
|
+
"dontAsk": "read-only",
|
|
39
|
+
"acceptEdits": "workspace-write",
|
|
40
|
+
"auto": "workspace-write",
|
|
41
|
+
"bypassPermissions": "danger-full-access",
|
|
42
|
+
}
|
|
43
|
+
# An unrecognised / unset mode: let the agent do work but stay sandboxed.
|
|
44
|
+
_DEFAULT_SANDBOX = "workspace-write"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _LaunchMixin:
|
|
48
|
+
"""Builds the per-turn `codex exec` command line."""
|
|
49
|
+
|
|
50
|
+
def frame_input(self, message: dict) -> dict:
|
|
51
|
+
# Never called: `codex exec` is ephemeral (persistent_process=False) —
|
|
52
|
+
# the turn's prompt rides in argv via LaunchOptions.prompt, so nothing
|
|
53
|
+
# is ever framed for stdin. Raise so a mis-wired send fails loudly.
|
|
54
|
+
raise NotImplementedError(
|
|
55
|
+
"codex exec embeds the prompt in argv; it has no stdin protocol")
|
|
56
|
+
|
|
57
|
+
def build_command(self, opts: LaunchOptions) -> list[str]:
|
|
58
|
+
cmd = [self.binary(), "exec"]
|
|
59
|
+
|
|
60
|
+
# Resume an existing thread, else start a fresh one.
|
|
61
|
+
if opts.session_id:
|
|
62
|
+
cmd += ["resume", opts.session_id]
|
|
63
|
+
|
|
64
|
+
cmd += ["--json", "--skip-git-repo-check"]
|
|
65
|
+
|
|
66
|
+
# Pass `-m` only for a model THIS engine actually offers; otherwise let
|
|
67
|
+
# Codex use its configured default (from `codex login` /
|
|
68
|
+
# ~/.codex/config.toml). The session's model hierarchy defaults to a
|
|
69
|
+
# *Claude* id, which isn't in Codex's catalog, so it's never forwarded —
|
|
70
|
+
# and when a real Codex catalog is registered, `-m` starts working with
|
|
71
|
+
# no change here.
|
|
72
|
+
forwarded_model = None
|
|
73
|
+
if opts.model and any(m.get("id") == opts.model for m in self.models()):
|
|
74
|
+
forwarded_model = opts.model
|
|
75
|
+
cmd += ["-m", opts.model]
|
|
76
|
+
|
|
77
|
+
if opts.effort:
|
|
78
|
+
# Clamp to what the model that will actually run supports (the
|
|
79
|
+
# configured default when no -m is forwarded → conservative range).
|
|
80
|
+
mapped = self.effort_for_model(opts.effort, forwarded_model)
|
|
81
|
+
if mapped:
|
|
82
|
+
# The config key is `model_reasoning_effort` — a bare
|
|
83
|
+
# `reasoning_effort` override is silently ignored by Codex.
|
|
84
|
+
cmd += ["-c", f"model_reasoning_effort={mapped}"]
|
|
85
|
+
|
|
86
|
+
# Sandbox policy. The top-level `codex exec` takes `-s <mode>`, but the
|
|
87
|
+
# `resume` subcommand has no `-s` flag — there the policy is a config
|
|
88
|
+
# override (`-c sandbox_mode=...`, parsed as TOML / raw string).
|
|
89
|
+
sandbox = _SANDBOX_BY_MODE.get(opts.permission_mode or "", _DEFAULT_SANDBOX)
|
|
90
|
+
if opts.session_id:
|
|
91
|
+
cmd += ["-c", f'sandbox_mode="{sandbox}"']
|
|
92
|
+
else:
|
|
93
|
+
cmd += ["-s", sandbox]
|
|
94
|
+
|
|
95
|
+
# Images: write base64 uploads to temp files and pass their paths.
|
|
96
|
+
image_paths = self._materialize_images(opts.images)
|
|
97
|
+
if image_paths:
|
|
98
|
+
cmd += ["-i", ",".join(image_paths)]
|
|
99
|
+
|
|
100
|
+
# Prompt is the final positional argument. Empty string is valid
|
|
101
|
+
# (e.g. an API-retry "continue" turn with no new text).
|
|
102
|
+
cmd.append(opts.prompt or "")
|
|
103
|
+
|
|
104
|
+
return cmd
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _materialize_images(images: Optional[list]) -> list[str]:
|
|
108
|
+
"""Decode base64 upload objects to temp files; return their paths."""
|
|
109
|
+
if not images:
|
|
110
|
+
return []
|
|
111
|
+
import base64
|
|
112
|
+
import tempfile
|
|
113
|
+
import uuid
|
|
114
|
+
out_dir = paths.DATA_HOME / "tmp" / "codex-images"
|
|
115
|
+
try:
|
|
116
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
except OSError:
|
|
118
|
+
out_dir = None
|
|
119
|
+
paths: list[str] = []
|
|
120
|
+
for img in images:
|
|
121
|
+
source = img.get("source", {}) if isinstance(img, dict) else {}
|
|
122
|
+
if source.get("type") != "base64" or not source.get("data"):
|
|
123
|
+
continue
|
|
124
|
+
media_type = source.get("media_type", "image/png")
|
|
125
|
+
ext = "jpg" if "jpeg" in media_type else media_type.split("/")[-1]
|
|
126
|
+
name = f"img_{uuid.uuid4().hex[:10]}.{ext}"
|
|
127
|
+
try:
|
|
128
|
+
data = base64.b64decode(source["data"])
|
|
129
|
+
if out_dir is not None:
|
|
130
|
+
p = out_dir / name
|
|
131
|
+
p.write_bytes(data)
|
|
132
|
+
else: # fall back to the system temp dir
|
|
133
|
+
fd = tempfile.NamedTemporaryFile(
|
|
134
|
+
prefix="codex_img_", suffix=f".{ext}", delete=False
|
|
135
|
+
)
|
|
136
|
+
fd.write(data)
|
|
137
|
+
fd.close()
|
|
138
|
+
p = fd.name
|
|
139
|
+
paths.append(str(p))
|
|
140
|
+
except (OSError, ValueError):
|
|
141
|
+
continue
|
|
142
|
+
return paths
|