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,202 @@
|
|
|
1
|
+
"""Codex app-server provider — launch argv + JSON-RPC param shaping.
|
|
2
|
+
|
|
3
|
+
`build_command` is trivial (`codex app-server --stdio`); the interesting part is
|
|
4
|
+
turning resolved `LaunchOptions` + a canonical user message into the
|
|
5
|
+
`thread/start` / `turn/start` JSON-RPC params. That shaping lives here (the
|
|
6
|
+
provider owns everything CLI-shaped) so the transport driver stays pure plumbing
|
|
7
|
+
— it just calls `provider.thread_start_params(...)` etc. Mixed into
|
|
8
|
+
`CodexAppServerProvider`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from painapple_code import paths
|
|
16
|
+
from painapple_code.providers.base import LaunchOptions
|
|
17
|
+
|
|
18
|
+
# Effort → `turn/start.effort` (ReasoningEffort) goes through capabilities'
|
|
19
|
+
# effort_for_model(): the CLI's own models_cache declares each model's
|
|
20
|
+
# supported reasoning levels, and the requested level is clamped to the
|
|
21
|
+
# TARGET model's range — mirroring codex exec (unknown/default model → the
|
|
22
|
+
# range every listed model speaks; no cache → the low/medium/high triad).
|
|
23
|
+
|
|
24
|
+
# Permission value → app-server `SandboxMode` (thread/start.sandbox). The
|
|
25
|
+
# app-server's native vocabulary is identical to codex exec's, so new sessions
|
|
26
|
+
# (which store the native value) map to themselves; the Claude-vocabulary rows
|
|
27
|
+
# are a back-compat shim for sessions created before native modes existed.
|
|
28
|
+
_SANDBOX_BY_MODE = {
|
|
29
|
+
# Codex-native (identity)
|
|
30
|
+
"read-only": "read-only",
|
|
31
|
+
"workspace-write": "workspace-write",
|
|
32
|
+
"danger-full-access": "danger-full-access",
|
|
33
|
+
# Legacy Claude-vocabulary (pre-native sessions)
|
|
34
|
+
"plan": "read-only",
|
|
35
|
+
"dontAsk": "read-only",
|
|
36
|
+
"acceptEdits": "workspace-write",
|
|
37
|
+
"auto": "workspace-write",
|
|
38
|
+
"bypassPermissions": "danger-full-access",
|
|
39
|
+
}
|
|
40
|
+
_DEFAULT_SANDBOX = "workspace-write"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _LaunchMixin:
|
|
44
|
+
"""Builds the app-server launch argv and the JSON-RPC turn/thread params."""
|
|
45
|
+
|
|
46
|
+
def frame_input(self, message: dict) -> dict:
|
|
47
|
+
# Never called: turns go over the JSON-RPC transport (`send_turn`), not
|
|
48
|
+
# the stdin line path. Raise so a mis-wired send fails loudly.
|
|
49
|
+
raise NotImplementedError(
|
|
50
|
+
"codex app-server turns ride the JSON-RPC transport, not stdin lines")
|
|
51
|
+
|
|
52
|
+
def build_command(self, opts: LaunchOptions) -> list[str]:
|
|
53
|
+
# Persistent JSON-RPC process over stdio. Model/effort/sandbox are NOT
|
|
54
|
+
# argv here — they ride on the thread/start + turn/start params below.
|
|
55
|
+
return [self.binary(), "app-server", "--stdio"]
|
|
56
|
+
|
|
57
|
+
# --- JSON-RPC param shaping (called by the transport) -----------------
|
|
58
|
+
|
|
59
|
+
def thread_start_params(self, opts: LaunchOptions, cwd: str) -> dict:
|
|
60
|
+
"""Params for a fresh `thread/start`.
|
|
61
|
+
|
|
62
|
+
P1 sets `approvalPolicy="never"`: the sandbox alone governs what
|
|
63
|
+
model-generated commands may do, exactly like `codex exec` (which runs
|
|
64
|
+
approvals-off). Interactive approvals are a later phase.
|
|
65
|
+
"""
|
|
66
|
+
params: dict = {
|
|
67
|
+
"cwd": cwd,
|
|
68
|
+
"sandbox": _SANDBOX_BY_MODE.get(opts.permission_mode or "", _DEFAULT_SANDBOX),
|
|
69
|
+
"approvalPolicy": "never",
|
|
70
|
+
}
|
|
71
|
+
model = self._wire_model(opts.model)
|
|
72
|
+
if model:
|
|
73
|
+
params["model"] = model
|
|
74
|
+
return params
|
|
75
|
+
|
|
76
|
+
def thread_resume_params(self, opts: LaunchOptions, thread_id: str, cwd: str) -> dict:
|
|
77
|
+
"""Params for `thread/resume` (continue an existing thread by id)."""
|
|
78
|
+
params: dict = {"threadId": thread_id, "cwd": cwd}
|
|
79
|
+
model = self._wire_model(opts.model)
|
|
80
|
+
if model:
|
|
81
|
+
params["model"] = model
|
|
82
|
+
return params
|
|
83
|
+
|
|
84
|
+
def thread_fork_params(self, opts: LaunchOptions, fork_from_id: str, cwd: str) -> dict:
|
|
85
|
+
"""Params for `thread/fork` — branch a source thread into a fresh one.
|
|
86
|
+
|
|
87
|
+
The native equivalent of Claude's `--resume <id> --fork-session`: the
|
|
88
|
+
app-server loads the source thread from disk and forks it into a new
|
|
89
|
+
*persisted* thread (no rollout-file copy). Deliberately not `ephemeral`
|
|
90
|
+
— the fork becomes the new session's thread, captured from the
|
|
91
|
+
response and resumed on subsequent turns. Sandbox/approval/model mirror
|
|
92
|
+
`thread_start_params` so the branch inherits the session's resolved
|
|
93
|
+
settings. `sandbox` and `permissions` are mutually exclusive in the
|
|
94
|
+
schema; we use `sandbox` (our native vocabulary), so `permissions` is
|
|
95
|
+
omitted.
|
|
96
|
+
"""
|
|
97
|
+
params: dict = {
|
|
98
|
+
"threadId": fork_from_id,
|
|
99
|
+
"cwd": cwd,
|
|
100
|
+
"sandbox": _SANDBOX_BY_MODE.get(opts.permission_mode or "", _DEFAULT_SANDBOX),
|
|
101
|
+
"approvalPolicy": "never",
|
|
102
|
+
}
|
|
103
|
+
model = self._wire_model(opts.model)
|
|
104
|
+
if model:
|
|
105
|
+
params["model"] = model
|
|
106
|
+
return params
|
|
107
|
+
|
|
108
|
+
def turn_start_params(self, opts: LaunchOptions, thread_id: str, input_items: list) -> dict:
|
|
109
|
+
"""Params for `turn/start` — the per-turn input plus effort/model."""
|
|
110
|
+
# `summary` opts in to reasoning summaries — WITHOUT it the app-server
|
|
111
|
+
# emits reasoning items with empty summary/content arrays (thinking
|
|
112
|
+
# blocks would render blank). "auto" lets the backend pick the format,
|
|
113
|
+
# matching the codex TUI's own default.
|
|
114
|
+
params: dict = {"threadId": thread_id, "input": input_items,
|
|
115
|
+
"summary": "auto"}
|
|
116
|
+
model = self._wire_model(opts.model)
|
|
117
|
+
if opts.effort:
|
|
118
|
+
# Clamp to the range the model that will actually run supports
|
|
119
|
+
# (forwarded pick, else the conservative default-model range).
|
|
120
|
+
mapped = self.effort_for_model(opts.effort, model)
|
|
121
|
+
if mapped:
|
|
122
|
+
params["effort"] = mapped
|
|
123
|
+
if model:
|
|
124
|
+
params["model"] = model
|
|
125
|
+
return params
|
|
126
|
+
|
|
127
|
+
def build_turn_input(self, message: dict) -> list[dict]:
|
|
128
|
+
"""Canonical user message → app-server `UserInput[]`.
|
|
129
|
+
|
|
130
|
+
The canonical shape (built in ws_chat) is
|
|
131
|
+
``{"type":"user","message":{"role":"user","content": str | [blocks]}}``.
|
|
132
|
+
Text becomes a ``text`` input; base64 image blocks are materialized to
|
|
133
|
+
temp files and passed as ``localImage`` inputs.
|
|
134
|
+
"""
|
|
135
|
+
content = (message or {}).get("message", {}).get("content", "")
|
|
136
|
+
items: list[dict] = []
|
|
137
|
+
if isinstance(content, str):
|
|
138
|
+
if content:
|
|
139
|
+
items.append({"type": "text", "text": content})
|
|
140
|
+
return items
|
|
141
|
+
if isinstance(content, list):
|
|
142
|
+
image_sources = []
|
|
143
|
+
for block in content:
|
|
144
|
+
if not isinstance(block, dict):
|
|
145
|
+
continue
|
|
146
|
+
if block.get("type") == "text" and block.get("text"):
|
|
147
|
+
items.append({"type": "text", "text": block["text"]})
|
|
148
|
+
elif block.get("type") == "image":
|
|
149
|
+
image_sources.append(block)
|
|
150
|
+
for path in self._materialize_images(image_sources):
|
|
151
|
+
items.append({"type": "localImage", "path": path})
|
|
152
|
+
return items
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _wire_model(model: Optional[str]) -> Optional[str]:
|
|
156
|
+
"""Only forward a model the app-server would accept.
|
|
157
|
+
|
|
158
|
+
The session model hierarchy defaults to a *Claude* id (which Codex would
|
|
159
|
+
reject); never forward that — let the app-server use the model configured
|
|
160
|
+
by `codex login` / config.toml. A non-Claude id is passed through.
|
|
161
|
+
"""
|
|
162
|
+
if model and not model.startswith("claude"):
|
|
163
|
+
return model
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _materialize_images(images: Optional[list]) -> list[str]:
|
|
168
|
+
"""Decode base64 image blocks to temp files; return their paths."""
|
|
169
|
+
if not images:
|
|
170
|
+
return []
|
|
171
|
+
import base64
|
|
172
|
+
import tempfile
|
|
173
|
+
import uuid
|
|
174
|
+
out_dir = paths.DATA_HOME / "tmp" / "codex-images"
|
|
175
|
+
try:
|
|
176
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
except OSError:
|
|
178
|
+
out_dir = None
|
|
179
|
+
paths: list[str] = []
|
|
180
|
+
for img in images:
|
|
181
|
+
source = img.get("source", {}) if isinstance(img, dict) else {}
|
|
182
|
+
if source.get("type") != "base64" or not source.get("data"):
|
|
183
|
+
continue
|
|
184
|
+
media_type = source.get("media_type", "image/png")
|
|
185
|
+
ext = "jpg" if "jpeg" in media_type else media_type.split("/")[-1]
|
|
186
|
+
name = f"img_{uuid.uuid4().hex[:10]}.{ext}"
|
|
187
|
+
try:
|
|
188
|
+
data = base64.b64decode(source["data"])
|
|
189
|
+
if out_dir is not None:
|
|
190
|
+
p = out_dir / name
|
|
191
|
+
p.write_bytes(data)
|
|
192
|
+
else:
|
|
193
|
+
fd = tempfile.NamedTemporaryFile(
|
|
194
|
+
prefix="codex_img_", suffix=f".{ext}", delete=False
|
|
195
|
+
)
|
|
196
|
+
fd.write(data)
|
|
197
|
+
fd.close()
|
|
198
|
+
p = fd.name
|
|
199
|
+
paths.append(str(p))
|
|
200
|
+
except (OSError, ValueError):
|
|
201
|
+
continue
|
|
202
|
+
return paths
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Codex app-server provider — native rich-commit summary over JSON-RPC.
|
|
2
|
+
|
|
3
|
+
The sibling `codex` exec provider summarizes a turn by copying the thread's
|
|
4
|
+
rollout JSONL under a fresh id and running `codex exec resume` on the copy
|
|
5
|
+
(`codex exec resume` writes in place, so it can't summarize the real thread
|
|
6
|
+
without polluting it). The app-server needs no such file copy: it can
|
|
7
|
+
`thread/fork` the real thread into an *ephemeral* branch — a throwaway thread
|
|
8
|
+
that never persists to disk — and run one `turn/start` with a native
|
|
9
|
+
`outputSchema` to get the structured sections back. No rollout copy, no temp
|
|
10
|
+
schema/output files, no pollution of the user's thread.
|
|
11
|
+
|
|
12
|
+
Isolation: the summary runs on its OWN short-lived `codex app-server` process
|
|
13
|
+
(spawned by the shadow-git layer from `build_summary_fork`'s argv), not the
|
|
14
|
+
user's live session transport — so its turn notifications never interleave with
|
|
15
|
+
the user's chat stream. `drive_summary_fork` conducts that process's JSON-RPC
|
|
16
|
+
conversation and returns the structured sections + token usage.
|
|
17
|
+
|
|
18
|
+
On any failure it returns `(None, None)`: the turn still gets a basic shadow
|
|
19
|
+
commit, just without AI sections — exactly like any other unavailable fork.
|
|
20
|
+
Subclasses the exec summary mixin only to reuse `_strictify_schema` (the
|
|
21
|
+
app-server's `outputSchema` runs in the same OpenAI strict mode as exec's
|
|
22
|
+
`--output-schema`). Mixed into `CodexAppServerProvider`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from painapple_code import paths
|
|
33
|
+
from painapple_code.providers.base import SummaryForkPlan
|
|
34
|
+
from painapple_code.providers.codex.summary import _SummaryMixin as _CodexExecSummaryMixin
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("painapple_code")
|
|
37
|
+
|
|
38
|
+
# Per-request waits. The handshake/fork/turn acks return promptly; the turn's
|
|
39
|
+
# content streams as notifications, so awaiting an ack never blocks turn-length.
|
|
40
|
+
_RPC_TIMEOUT = 60.0
|
|
41
|
+
# Whole-turn wait for the streamed result; the shadow-git layer also caps the
|
|
42
|
+
# entire fork at 300s, so this stays under that.
|
|
43
|
+
_TURN_TIMEOUT = 240.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _SummaryMixin(_CodexExecSummaryMixin):
|
|
47
|
+
"""Native app-server rich-commit fork — ephemeral thread/fork + outputSchema."""
|
|
48
|
+
|
|
49
|
+
def build_summary_fork(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
session_id: str,
|
|
53
|
+
fork_prompt: str,
|
|
54
|
+
schema_json: str,
|
|
55
|
+
cwd: str,
|
|
56
|
+
token_profile: Optional[str] = None,
|
|
57
|
+
session_model: Optional[str] = None,
|
|
58
|
+
) -> Optional[SummaryForkPlan]:
|
|
59
|
+
"""Plan a native (transport-driven) summary — no rollout copy.
|
|
60
|
+
|
|
61
|
+
The plan's argv launches a throwaway `codex app-server`; its `payload`
|
|
62
|
+
carries what `drive_summary_fork` needs: the source thread id to fork,
|
|
63
|
+
the prompt, the (strict) output schema, an optional cheaper summary
|
|
64
|
+
model, and the cwd. Nothing is copied to disk → empty `cleanup_paths`.
|
|
65
|
+
"""
|
|
66
|
+
if not session_id:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
schema_obj = json.loads(self._strictify_schema(schema_json))
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
return None
|
|
72
|
+
# Optional cheaper summarizer model (seam-described override);
|
|
73
|
+
# default inherits the thread's model.
|
|
74
|
+
summary_model = self.get_summary_model_override()
|
|
75
|
+
if summary_model and summary_model.startswith("claude"):
|
|
76
|
+
summary_model = None
|
|
77
|
+
return SummaryForkPlan(
|
|
78
|
+
argv=[self.binary(), "app-server", "--stdio"],
|
|
79
|
+
env=None, # codex uses ambient auth
|
|
80
|
+
model=summary_model or "codex (app-server)",
|
|
81
|
+
output_file=None,
|
|
82
|
+
cleanup_paths=[], # nothing copied — that's the win
|
|
83
|
+
payload={
|
|
84
|
+
"fork_from": session_id,
|
|
85
|
+
"prompt": fork_prompt,
|
|
86
|
+
"schema": schema_obj,
|
|
87
|
+
"model": summary_model,
|
|
88
|
+
"cwd": cwd,
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def drive_summary_fork(self, proc, plan):
|
|
93
|
+
"""Conduct the throwaway app-server's JSON-RPC summary conversation.
|
|
94
|
+
|
|
95
|
+
initialize → thread/fork{ephemeral} → turn/start{outputSchema} → collect
|
|
96
|
+
the final structured agentMessage + token usage. Returns
|
|
97
|
+
`(structured_data, cost)` or `(None, None)` on any failure (→ no AI
|
|
98
|
+
sections this turn). Leaves the process terminated.
|
|
99
|
+
"""
|
|
100
|
+
# Lazy import keeps the transport (and its asyncio plumbing) out of the
|
|
101
|
+
# package's static-description import path — same reason `make_transport`
|
|
102
|
+
# imports it lazily.
|
|
103
|
+
from painapple_code.providers.codex_app_server.transport import (
|
|
104
|
+
_APPROVAL_DENY, _client_version, JsonRpcTransport,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
payload = plan.payload or {}
|
|
108
|
+
fork_from = payload.get("fork_from")
|
|
109
|
+
if not fork_from:
|
|
110
|
+
return None, None
|
|
111
|
+
|
|
112
|
+
client = _SummaryRpcClient(proc, deny_map=_APPROVAL_DENY)
|
|
113
|
+
await client.start()
|
|
114
|
+
try:
|
|
115
|
+
await client.request("initialize", {
|
|
116
|
+
"clientInfo": {"name": "painapple-code", "version": _client_version()},
|
|
117
|
+
}, timeout=_RPC_TIMEOUT)
|
|
118
|
+
await client.notify("initialized")
|
|
119
|
+
|
|
120
|
+
fork_params = {
|
|
121
|
+
"threadId": fork_from,
|
|
122
|
+
"ephemeral": True, # throwaway branch — never persisted
|
|
123
|
+
"cwd": payload.get("cwd") or ".",
|
|
124
|
+
"sandbox": "read-only", # the summarizer must not touch files
|
|
125
|
+
"approvalPolicy": "never",
|
|
126
|
+
}
|
|
127
|
+
if payload.get("model"):
|
|
128
|
+
fork_params["model"] = payload["model"]
|
|
129
|
+
res = await client.request("thread/fork", fork_params, timeout=_RPC_TIMEOUT)
|
|
130
|
+
fork_id = JsonRpcTransport._thread_id_from_result(res)
|
|
131
|
+
if not fork_id:
|
|
132
|
+
return None, None
|
|
133
|
+
|
|
134
|
+
turn_params = {
|
|
135
|
+
"threadId": fork_id,
|
|
136
|
+
"input": [{"type": "text", "text": payload.get("prompt") or ""}],
|
|
137
|
+
"outputSchema": payload.get("schema"),
|
|
138
|
+
"effort": "low", # summarization needs no deep reasoning
|
|
139
|
+
}
|
|
140
|
+
if payload.get("model"):
|
|
141
|
+
turn_params["model"] = payload["model"]
|
|
142
|
+
await client.request("turn/start", turn_params, timeout=_RPC_TIMEOUT)
|
|
143
|
+
|
|
144
|
+
structured, usage = await client.await_turn(timeout=_TURN_TIMEOUT)
|
|
145
|
+
if not isinstance(structured, dict):
|
|
146
|
+
return None, None
|
|
147
|
+
cost = {
|
|
148
|
+
"cost": 0.0, # app-server reports no USD cost
|
|
149
|
+
"input_tokens": (usage or {}).get("inputTokens", 0) or 0,
|
|
150
|
+
"output_tokens": (usage or {}).get("outputTokens", 0) or 0,
|
|
151
|
+
}
|
|
152
|
+
return structured, cost
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.info(f"codex app-server summary fork failed: {e}")
|
|
155
|
+
return None, None
|
|
156
|
+
finally:
|
|
157
|
+
await client.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class _SummaryRpcClient:
|
|
161
|
+
"""Minimal one-shot JSON-RPC client over a throwaway app-server process.
|
|
162
|
+
|
|
163
|
+
Distinct from `JsonRpcTransport` (session-coupled, driven externally by the
|
|
164
|
+
session's stdout reader): this owns its own read loop and collects only what
|
|
165
|
+
a summary needs — the final `agentMessage` text (the structured JSON produced
|
|
166
|
+
under `outputSchema`) and the latest token usage — resolving `await_turn` on
|
|
167
|
+
`turn/completed`.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, proc, deny_map=None):
|
|
171
|
+
self.proc = proc
|
|
172
|
+
self._deny = deny_map or {}
|
|
173
|
+
self._id = 0
|
|
174
|
+
self._pending: dict = {}
|
|
175
|
+
self._turn = None # future resolved on turn/completed|error|EOF
|
|
176
|
+
self._last_text = None # last agentMessage text == the structured JSON
|
|
177
|
+
self._usage: dict = {}
|
|
178
|
+
self._reader = None
|
|
179
|
+
|
|
180
|
+
async def start(self):
|
|
181
|
+
self._reader = asyncio.create_task(self._read_loop())
|
|
182
|
+
|
|
183
|
+
def _next_id(self) -> int:
|
|
184
|
+
self._id += 1
|
|
185
|
+
return self._id
|
|
186
|
+
|
|
187
|
+
async def _write(self, obj: dict):
|
|
188
|
+
self.proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
|
|
189
|
+
await self.proc.stdin.drain()
|
|
190
|
+
|
|
191
|
+
async def request(self, method: str, params: dict, timeout: float = _RPC_TIMEOUT):
|
|
192
|
+
rid = self._next_id()
|
|
193
|
+
fut = asyncio.get_running_loop().create_future()
|
|
194
|
+
self._pending[rid] = fut
|
|
195
|
+
await self._write({"jsonrpc": "2.0", "id": rid, "method": method, "params": params})
|
|
196
|
+
try:
|
|
197
|
+
return await asyncio.wait_for(fut, timeout)
|
|
198
|
+
finally:
|
|
199
|
+
self._pending.pop(rid, None)
|
|
200
|
+
|
|
201
|
+
async def notify(self, method: str, params: dict | None = None):
|
|
202
|
+
msg = {"jsonrpc": "2.0", "method": method}
|
|
203
|
+
if params is not None:
|
|
204
|
+
msg["params"] = params
|
|
205
|
+
await self._write(msg)
|
|
206
|
+
|
|
207
|
+
async def await_turn(self, timeout: float = _TURN_TIMEOUT):
|
|
208
|
+
"""Wait for turn/completed; return (structured_or_None, usage)."""
|
|
209
|
+
self._turn = asyncio.get_running_loop().create_future()
|
|
210
|
+
try:
|
|
211
|
+
await asyncio.wait_for(self._turn, timeout)
|
|
212
|
+
except asyncio.TimeoutError:
|
|
213
|
+
return None, self._usage
|
|
214
|
+
structured = None
|
|
215
|
+
if self._last_text:
|
|
216
|
+
try:
|
|
217
|
+
structured = json.loads(self._last_text)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
structured = None
|
|
220
|
+
return structured, self._usage
|
|
221
|
+
|
|
222
|
+
async def _read_loop(self):
|
|
223
|
+
stdout = self.proc.stdout
|
|
224
|
+
while True:
|
|
225
|
+
line = await stdout.readline()
|
|
226
|
+
if not line: # EOF — process exited
|
|
227
|
+
self._finish_turn()
|
|
228
|
+
return
|
|
229
|
+
try:
|
|
230
|
+
msg = json.loads(line.decode(errors="replace").strip())
|
|
231
|
+
except (json.JSONDecodeError, ValueError):
|
|
232
|
+
continue
|
|
233
|
+
self._dispatch(msg)
|
|
234
|
+
|
|
235
|
+
def _dispatch(self, msg: dict):
|
|
236
|
+
has_id = msg.get("id") is not None
|
|
237
|
+
has_method = "method" in msg
|
|
238
|
+
if has_id and has_method: # server → client request: decline
|
|
239
|
+
asyncio.create_task(self._decline(msg))
|
|
240
|
+
return
|
|
241
|
+
if has_id and not has_method: # response to one of our requests
|
|
242
|
+
fut = self._pending.get(msg["id"])
|
|
243
|
+
if fut is not None and not fut.done():
|
|
244
|
+
if msg.get("error") is not None:
|
|
245
|
+
fut.set_exception(RuntimeError(str(msg["error"])))
|
|
246
|
+
else:
|
|
247
|
+
fut.set_result(msg.get("result"))
|
|
248
|
+
return
|
|
249
|
+
method = msg.get("method") # notification
|
|
250
|
+
params = msg.get("params") or {}
|
|
251
|
+
if method == "item/completed":
|
|
252
|
+
item = params.get("item") or {}
|
|
253
|
+
if item.get("type") == "agentMessage" and item.get("text"):
|
|
254
|
+
self._last_text = item["text"]
|
|
255
|
+
elif method == "thread/tokenUsage/updated":
|
|
256
|
+
tu = params.get("tokenUsage") or {}
|
|
257
|
+
self._usage = tu.get("last") or self._usage
|
|
258
|
+
elif method in ("turn/completed", "error"):
|
|
259
|
+
self._finish_turn()
|
|
260
|
+
|
|
261
|
+
def _finish_turn(self):
|
|
262
|
+
if self._turn is not None and not self._turn.done():
|
|
263
|
+
self._turn.set_result(None)
|
|
264
|
+
|
|
265
|
+
async def _decline(self, msg: dict):
|
|
266
|
+
rid = msg.get("id")
|
|
267
|
+
deny = self._deny.get(msg.get("method", ""))
|
|
268
|
+
try:
|
|
269
|
+
if deny is not None:
|
|
270
|
+
await self._write({"jsonrpc": "2.0", "id": rid, "result": deny})
|
|
271
|
+
else:
|
|
272
|
+
await self._write({
|
|
273
|
+
"jsonrpc": "2.0", "id": rid,
|
|
274
|
+
"error": {"code": -32601, "message": "not handled"},
|
|
275
|
+
})
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
async def close(self):
|
|
280
|
+
if self._reader is not None:
|
|
281
|
+
self._reader.cancel()
|
|
282
|
+
try:
|
|
283
|
+
await self._reader
|
|
284
|
+
except (asyncio.CancelledError, Exception):
|
|
285
|
+
pass
|
|
286
|
+
if self.proc.returncode is None:
|
|
287
|
+
try:
|
|
288
|
+
self.proc.kill()
|
|
289
|
+
await self.proc.wait()
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|