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.
Files changed (436) hide show
  1. painapple_code/__init__.py +30 -0
  2. painapple_code/__main__.py +18 -0
  3. painapple_code/_version.py +24 -0
  4. painapple_code/auth_middleware.py +930 -0
  5. painapple_code/cli/__init__.py +204 -0
  6. painapple_code/cli/deploy/__init__.py +15 -0
  7. painapple_code/cli/deploy/claude_seed.py +41 -0
  8. painapple_code/cli/deploy/config.py +260 -0
  9. painapple_code/cli/deploy/container.py +715 -0
  10. painapple_code/cli/deploy/launch.py +163 -0
  11. painapple_code/cli/deploy/runtime.py +223 -0
  12. painapple_code/cli/lifecycle_cmd.py +443 -0
  13. painapple_code/cli/list_cmd.py +572 -0
  14. painapple_code/cli/manage_cmd.py +670 -0
  15. painapple_code/cli/netinfo.py +125 -0
  16. painapple_code/cli/profiles.py +390 -0
  17. painapple_code/cli/serve_args.py +163 -0
  18. painapple_code/cli/serve_config.py +256 -0
  19. painapple_code/cli/setup_cmd.py +664 -0
  20. painapple_code/cli/ui.py +438 -0
  21. painapple_code/cost_analytics.py +879 -0
  22. painapple_code/data/models.defaults.yaml +18 -0
  23. painapple_code/data/models.yaml +18 -0
  24. painapple_code/data/presets.defaults.json +32 -0
  25. painapple_code/data/strings.yaml +2269 -0
  26. painapple_code/helpers.py +406 -0
  27. painapple_code/paths.py +1514 -0
  28. painapple_code/prompt_explorer.py +984 -0
  29. painapple_code/providers/__init__.py +224 -0
  30. painapple_code/providers/base.py +710 -0
  31. painapple_code/providers/claude/__init__.py +74 -0
  32. painapple_code/providers/claude/capabilities.py +218 -0
  33. painapple_code/providers/claude/errors.py +79 -0
  34. painapple_code/providers/claude/launch.py +50 -0
  35. painapple_code/providers/claude/summary.py +89 -0
  36. painapple_code/providers/claude/translate.py +74 -0
  37. painapple_code/providers/claude_sdk/__init__.py +139 -0
  38. painapple_code/providers/claude_sdk/driver.py +412 -0
  39. painapple_code/providers/codex/__init__.py +87 -0
  40. painapple_code/providers/codex/capabilities.py +321 -0
  41. painapple_code/providers/codex/errors.py +70 -0
  42. painapple_code/providers/codex/launch.py +142 -0
  43. painapple_code/providers/codex/summary.py +196 -0
  44. painapple_code/providers/codex/translate.py +204 -0
  45. painapple_code/providers/codex_app_server/__init__.py +100 -0
  46. painapple_code/providers/codex_app_server/capabilities.py +41 -0
  47. painapple_code/providers/codex_app_server/launch.py +202 -0
  48. painapple_code/providers/codex_app_server/summary.py +291 -0
  49. painapple_code/providers/codex_app_server/translate.py +263 -0
  50. painapple_code/providers/codex_app_server/transport.py +238 -0
  51. painapple_code/routes/__init__.py +0 -0
  52. painapple_code/routes/api_agents.py +441 -0
  53. painapple_code/routes/api_app.py +368 -0
  54. painapple_code/routes/api_app_commit_sections.py +127 -0
  55. painapple_code/routes/api_app_config.py +997 -0
  56. painapple_code/routes/api_app_session_prefs.py +217 -0
  57. painapple_code/routes/api_browser.py +690 -0
  58. painapple_code/routes/api_commands.py +374 -0
  59. painapple_code/routes/api_costs.py +128 -0
  60. painapple_code/routes/api_drafts.py +139 -0
  61. painapple_code/routes/api_exec.py +149 -0
  62. painapple_code/routes/api_files.py +542 -0
  63. painapple_code/routes/api_git.py +657 -0
  64. painapple_code/routes/api_logs.py +649 -0
  65. painapple_code/routes/api_plugins.py +275 -0
  66. painapple_code/routes/api_project_config.py +418 -0
  67. painapple_code/routes/api_prompts.py +231 -0
  68. painapple_code/routes/api_providers.py +53 -0
  69. painapple_code/routes/api_search.py +411 -0
  70. painapple_code/routes/api_session_stash.py +234 -0
  71. painapple_code/routes/api_session_welcome.py +151 -0
  72. painapple_code/routes/api_sessions.py +742 -0
  73. painapple_code/routes/api_shadow.py +323 -0
  74. painapple_code/routes/api_shadow_db.py +512 -0
  75. painapple_code/routes/api_shadow_files.py +332 -0
  76. painapple_code/routes/api_shadow_search.py +476 -0
  77. painapple_code/routes/api_skills.py +610 -0
  78. painapple_code/routes/api_tasks.py +147 -0
  79. painapple_code/routes/api_terminal.py +540 -0
  80. painapple_code/routes/api_upload.py +293 -0
  81. painapple_code/routes/api_viewer.py +229 -0
  82. painapple_code/routes/dependencies.py +109 -0
  83. painapple_code/routes/ws_chat.py +837 -0
  84. painapple_code/server.py +2208 -0
  85. painapple_code/server_logging.py +263 -0
  86. painapple_code/services/__init__.py +0 -0
  87. painapple_code/services/agent_session.py +2738 -0
  88. painapple_code/session_store.py +253 -0
  89. painapple_code/session_store_core.py +879 -0
  90. painapple_code/shadow_db.py +830 -0
  91. painapple_code/shadow_db_plans.py +369 -0
  92. painapple_code/shadow_db_queries.py +595 -0
  93. painapple_code/shadow_db_schema.py +676 -0
  94. painapple_code/shadow_git.py +1296 -0
  95. painapple_code/shadow_git_frontmatter.py +314 -0
  96. painapple_code/shadow_git_sections.py +520 -0
  97. painapple_code/shadow_git_summary.py +462 -0
  98. painapple_code/shadow_parser.py +560 -0
  99. painapple_code/static/css/00-variables.css +276 -0
  100. painapple_code/static/css/01-base.css +245 -0
  101. painapple_code/static/css/02-tooltips.css +36 -0
  102. painapple_code/static/css/09-left-rail.css +448 -0
  103. painapple_code/static/css/10-header.css +777 -0
  104. painapple_code/static/css/11-connection-bar.css +120 -0
  105. painapple_code/static/css/20-messages.css +750 -0
  106. painapple_code/static/css/21-tool-blocks.css +2340 -0
  107. painapple_code/static/css/22-edit-diff.css +619 -0
  108. painapple_code/static/css/23-thinking.css +1584 -0
  109. painapple_code/static/css/24-background-tasks.css +359 -0
  110. painapple_code/static/css/25-context-block.css +252 -0
  111. painapple_code/static/css/26-session-setup.css +144 -0
  112. painapple_code/static/css/30-markdown.css +854 -0
  113. painapple_code/static/css/40-input.css +2418 -0
  114. painapple_code/static/css/41-autocomplete.css +127 -0
  115. painapple_code/static/css/42-activity-strip.css +97 -0
  116. painapple_code/static/css/43-file-autocomplete.css +121 -0
  117. painapple_code/static/css/44-snippets-autocomplete.css +224 -0
  118. painapple_code/static/css/45-selection-bar.css +433 -0
  119. painapple_code/static/css/46-keyboard-bar.css +119 -0
  120. painapple_code/static/css/47-keyboard-longpress.css +116 -0
  121. painapple_code/static/css/47-token-profile.css +106 -0
  122. painapple_code/static/css/50-welcome.css +3095 -0
  123. painapple_code/static/css/51-modals.css +301 -0
  124. painapple_code/static/css/52-utilities.css +77 -0
  125. painapple_code/static/css/53-image-upload.css +456 -0
  126. painapple_code/static/css/54-todo-list.css +102 -0
  127. painapple_code/static/css/55-file-upload.css +113 -0
  128. painapple_code/static/css/56-chat-navigator.css +218 -0
  129. painapple_code/static/css/56-permission-card.css +332 -0
  130. painapple_code/static/css/56-plan-approval.css +204 -0
  131. painapple_code/static/css/56-question-form.css +602 -0
  132. painapple_code/static/css/57-swipe-indicator.css +95 -0
  133. painapple_code/static/css/58-session-families.css +627 -0
  134. painapple_code/static/css/59-about.css +196 -0
  135. painapple_code/static/css/61-log-explorer.css +723 -0
  136. painapple_code/static/css/62-json-tree.css +489 -0
  137. painapple_code/static/css/62-skills-widget.css +650 -0
  138. painapple_code/static/css/63-commands-widget.css +30 -0
  139. painapple_code/static/css/63-plugins-widget.css +174 -0
  140. painapple_code/static/css/63-terminal.css +516 -0
  141. painapple_code/static/css/64-search-files-widget.css +448 -0
  142. painapple_code/static/css/65-snippets-widget.css +426 -0
  143. painapple_code/static/css/66-active-sessions.css +979 -0
  144. painapple_code/static/css/66-agents-widget.css +265 -0
  145. painapple_code/static/css/66-file-explorer-widget.css +855 -0
  146. painapple_code/static/css/66-git-panel.css +791 -0
  147. painapple_code/static/css/66-history-explorer.css +1496 -0
  148. painapple_code/static/css/67-diff-viewer.css +602 -0
  149. painapple_code/static/css/67-editor-view.css +748 -0
  150. painapple_code/static/css/67-file-preview-widget.css +1873 -0
  151. painapple_code/static/css/68-browser-widget.css +337 -0
  152. painapple_code/static/css/68-compare-wizard.css +355 -0
  153. painapple_code/static/css/68-csv-preview.css +106 -0
  154. painapple_code/static/css/68-diff-preview.css +103 -0
  155. painapple_code/static/css/68-helpers-install-widget.css +491 -0
  156. painapple_code/static/css/68-image-annotate.css +346 -0
  157. painapple_code/static/css/68-terminal-view.css +98 -0
  158. painapple_code/static/css/68-uploads-widget.css +229 -0
  159. painapple_code/static/css/69-cost-analytics.css +537 -0
  160. painapple_code/static/css/70-config-panel.css +3736 -0
  161. painapple_code/static/css/70-excalidraw.css +136 -0
  162. painapple_code/static/css/70-prompt-explorer.css +890 -0
  163. painapple_code/static/css/71-chart.css +111 -0
  164. painapple_code/static/css/72-discussion-sidebar.css +678 -0
  165. painapple_code/static/css/74-debug-logs.css +656 -0
  166. painapple_code/static/css/80-open-dialog.css +146 -0
  167. painapple_code/static/css/80-quick-switcher.css +383 -0
  168. painapple_code/static/css/80-widget-system.css +1763 -0
  169. painapple_code/static/css/81-quick-actions.css +1020 -0
  170. painapple_code/static/css/82-context-popover.css +1233 -0
  171. painapple_code/static/css/83-context-menu.css +339 -0
  172. painapple_code/static/css/85-lazy-loading.css +102 -0
  173. painapple_code/static/css/86-zen-mode.css +708 -0
  174. painapple_code/static/css/87-grid-switcher.css +346 -0
  175. painapple_code/static/css/login.css +198 -0
  176. painapple_code/static/dist/app.bundle.js +7177 -0
  177. painapple_code/static/feature-triage.html +446 -0
  178. painapple_code/static/icons/apple-touch-icon.png +0 -0
  179. painapple_code/static/icons/favicon-32.png +0 -0
  180. painapple_code/static/icons/favicon.ico +0 -0
  181. painapple_code/static/icons/icon-128.png +0 -0
  182. painapple_code/static/icons/icon-144.png +0 -0
  183. painapple_code/static/icons/icon-152.png +0 -0
  184. painapple_code/static/icons/icon-180.png +0 -0
  185. painapple_code/static/icons/icon-192.png +0 -0
  186. painapple_code/static/icons/icon-384.png +0 -0
  187. painapple_code/static/icons/icon-512.png +0 -0
  188. painapple_code/static/icons/icon-72.png +0 -0
  189. painapple_code/static/icons/icon-96.png +0 -0
  190. painapple_code/static/js/action-delegate.js +187 -0
  191. painapple_code/static/js/activity-strip.js +193 -0
  192. painapple_code/static/js/app/context-menus.js +619 -0
  193. painapple_code/static/js/app/file-preview.js +163 -0
  194. painapple_code/static/js/app/init-ui.js +574 -0
  195. painapple_code/static/js/app/input-events.js +561 -0
  196. painapple_code/static/js/app/input-resize.js +313 -0
  197. painapple_code/static/js/app/message-actions.js +419 -0
  198. painapple_code/static/js/app/panels.js +210 -0
  199. painapple_code/static/js/app/rendering-delegators.js +246 -0
  200. painapple_code/static/js/app/server-loading.js +365 -0
  201. painapple_code/static/js/app/session-ops.js +287 -0
  202. painapple_code/static/js/app/session-switch.js +443 -0
  203. painapple_code/static/js/app/settings-helpers.js +280 -0
  204. painapple_code/static/js/app/startup-welcome.js +290 -0
  205. painapple_code/static/js/app-context.js +157 -0
  206. painapple_code/static/js/app.js +2134 -0
  207. painapple_code/static/js/auth-fetch.js +46 -0
  208. painapple_code/static/js/background-tasks.js +274 -0
  209. painapple_code/static/js/boot.js +73 -0
  210. painapple_code/static/js/caret-position.js +171 -0
  211. painapple_code/static/js/chat-navigator.js +710 -0
  212. painapple_code/static/js/chat-search.js +323 -0
  213. painapple_code/static/js/command-executor.js +176 -0
  214. painapple_code/static/js/command-store.js +497 -0
  215. painapple_code/static/js/components.js +1414 -0
  216. painapple_code/static/js/config.js +218 -0
  217. painapple_code/static/js/context-menu.js +740 -0
  218. painapple_code/static/js/controllers/chat-controller.js +4252 -0
  219. painapple_code/static/js/controllers/dialog-controller.js +217 -0
  220. painapple_code/static/js/controllers/tab-controller.js +1828 -0
  221. painapple_code/static/js/controllers/thinking-controller.js +915 -0
  222. painapple_code/static/js/diff-utils.js +486 -0
  223. painapple_code/static/js/drafts-pill.js +95 -0
  224. painapple_code/static/js/editor-view.js +815 -0
  225. painapple_code/static/js/effort-settings.js +430 -0
  226. painapple_code/static/js/feature-triage.js +811 -0
  227. painapple_code/static/js/file-autocomplete.js +516 -0
  228. painapple_code/static/js/file-tabs.js +123 -0
  229. painapple_code/static/js/gestures.js +448 -0
  230. painapple_code/static/js/grid-switcher.js +334 -0
  231. painapple_code/static/js/input-handler.js +1472 -0
  232. painapple_code/static/js/keyboard-bar.js +495 -0
  233. painapple_code/static/js/layout-switcher.js +109 -0
  234. painapple_code/static/js/linkify-utils.js +200 -0
  235. painapple_code/static/js/login.js +141 -0
  236. painapple_code/static/js/open-dialog.js +879 -0
  237. painapple_code/static/js/orphan-terminals.js +373 -0
  238. painapple_code/static/js/path-utils.js +179 -0
  239. painapple_code/static/js/perf-marks.js +102 -0
  240. painapple_code/static/js/permission-settings.js +475 -0
  241. painapple_code/static/js/pointer-input.js +59 -0
  242. painapple_code/static/js/preview/json-tree-menu.js +226 -0
  243. painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
  244. painapple_code/static/js/preview/json-tree.js +457 -0
  245. painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
  246. painapple_code/static/js/preview/preview-edit.js +367 -0
  247. painapple_code/static/js/preview/preview-events.js +361 -0
  248. painapple_code/static/js/preview/preview-history.js +567 -0
  249. painapple_code/static/js/preview/preview-inline-edit.js +869 -0
  250. painapple_code/static/js/preview/preview-poll.js +108 -0
  251. painapple_code/static/js/preview/preview-render.js +572 -0
  252. painapple_code/static/js/preview/preview-search.js +421 -0
  253. painapple_code/static/js/preview/preview-state.js +256 -0
  254. painapple_code/static/js/preview/preview-utils.js +251 -0
  255. painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
  256. painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
  257. painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
  258. painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
  259. painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
  260. painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
  261. painapple_code/static/js/preview-plugins/index.js +34 -0
  262. painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
  263. painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
  264. painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
  265. painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
  266. painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
  267. painapple_code/static/js/project-color-picker.js +110 -0
  268. painapple_code/static/js/project-colors.js +163 -0
  269. painapple_code/static/js/prompt-favorites.js +187 -0
  270. painapple_code/static/js/quick-actions-menu.js +1736 -0
  271. painapple_code/static/js/quick-actions-registry.js +1239 -0
  272. painapple_code/static/js/quick-switcher/controller.js +294 -0
  273. painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
  274. painapple_code/static/js/quick-switcher/index.js +30 -0
  275. painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
  276. painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
  277. painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
  278. painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
  279. painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
  280. painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
  281. painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
  282. painapple_code/static/js/quick-switcher/registry.js +69 -0
  283. painapple_code/static/js/quick-switcher/ui/item.js +54 -0
  284. painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
  285. painapple_code/static/js/recent-opens.js +40 -0
  286. painapple_code/static/js/scroll-manager.js +530 -0
  287. painapple_code/static/js/scroll-state-machine.js +359 -0
  288. painapple_code/static/js/selection/action-bar.js +793 -0
  289. painapple_code/static/js/selection/index.js +15 -0
  290. painapple_code/static/js/selection/selection-handler.js +877 -0
  291. painapple_code/static/js/selection/state.js +181 -0
  292. painapple_code/static/js/session/agent-progress.js +202 -0
  293. painapple_code/static/js/session/handle-agent-message.js +481 -0
  294. painapple_code/static/js/session/handle-message.js +550 -0
  295. painapple_code/static/js/session/interactive.js +587 -0
  296. painapple_code/static/js/session/message-store.js +271 -0
  297. painapple_code/static/js/session/messages.js +253 -0
  298. painapple_code/static/js/session/persistence.js +179 -0
  299. painapple_code/static/js/session/restore.js +136 -0
  300. painapple_code/static/js/session/sync.js +333 -0
  301. painapple_code/static/js/session-container-pool.js +371 -0
  302. painapple_code/static/js/session-setup-panel.js +274 -0
  303. painapple_code/static/js/session.js +1047 -0
  304. painapple_code/static/js/shortcut-hints.js +272 -0
  305. painapple_code/static/js/shortcuts.js +1146 -0
  306. painapple_code/static/js/skills-autocomplete.js +262 -0
  307. painapple_code/static/js/snippets-autocomplete.js +738 -0
  308. painapple_code/static/js/stash-refs-view.js +83 -0
  309. painapple_code/static/js/stash-ui.js +768 -0
  310. painapple_code/static/js/stash.js +785 -0
  311. painapple_code/static/js/status-bar.js +1184 -0
  312. painapple_code/static/js/token-profile.js +274 -0
  313. painapple_code/static/js/tool-renderer-blocks.js +1626 -0
  314. painapple_code/static/js/tool-renderer-thinking.js +813 -0
  315. painapple_code/static/js/tool-renderer.js +1500 -0
  316. painapple_code/static/js/tooltips.js +197 -0
  317. painapple_code/static/js/upload-manager.js +1019 -0
  318. painapple_code/static/js/utils.js +521 -0
  319. painapple_code/static/js/welcome/api.js +363 -0
  320. painapple_code/static/js/welcome/cards.js +626 -0
  321. painapple_code/static/js/welcome/context-menu.js +585 -0
  322. painapple_code/static/js/welcome/families.js +801 -0
  323. painapple_code/static/js/welcome/preview.js +234 -0
  324. painapple_code/static/js/welcome/state.js +129 -0
  325. painapple_code/static/js/welcome.js +1874 -0
  326. painapple_code/static/js/widget-system/base-widget.js +717 -0
  327. painapple_code/static/js/widget-system/device-manager.js +178 -0
  328. painapple_code/static/js/widget-system/event-bus.js +152 -0
  329. painapple_code/static/js/widget-system/icons.js +120 -0
  330. painapple_code/static/js/widget-system/index.js +49 -0
  331. painapple_code/static/js/widget-system/init.js +54 -0
  332. painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
  333. painapple_code/static/js/widget-system/types/floating.js +915 -0
  334. painapple_code/static/js/widget-system/types/index.js +40 -0
  335. painapple_code/static/js/widget-system/types/modal.js +147 -0
  336. painapple_code/static/js/widget-system/types/sidebar.js +158 -0
  337. painapple_code/static/js/widget-system/types/tab.js +146 -0
  338. painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
  339. painapple_code/static/js/widget-system/widget-manager.js +1187 -0
  340. painapple_code/static/js/widgets/about-widget.js +260 -0
  341. painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
  342. painapple_code/static/js/widgets/agents-widget.js +897 -0
  343. painapple_code/static/js/widgets/browser-widget.js +579 -0
  344. painapple_code/static/js/widgets/commands-widget.js +694 -0
  345. painapple_code/static/js/widgets/config/commit-sections.js +449 -0
  346. painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
  347. painapple_code/static/js/widgets/config/gestures.js +161 -0
  348. painapple_code/static/js/widgets/config/models-tab.js +988 -0
  349. painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
  350. painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
  351. painapple_code/static/js/widgets/config/state.js +517 -0
  352. painapple_code/static/js/widgets/config/system-controls.js +90 -0
  353. painapple_code/static/js/widgets/config-widget.js +1535 -0
  354. painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
  355. painapple_code/static/js/widgets/debug-widget.js +693 -0
  356. painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
  357. painapple_code/static/js/widgets/discussion-widget.js +1107 -0
  358. painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
  359. painapple_code/static/js/widgets/file-preview-widget.js +861 -0
  360. painapple_code/static/js/widgets/git-widget.js +794 -0
  361. painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
  362. painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
  363. painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
  364. painapple_code/static/js/widgets/image-preview-widget.js +692 -0
  365. painapple_code/static/js/widgets/index.js +176 -0
  366. painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
  367. painapple_code/static/js/widgets/plugins-widget.js +305 -0
  368. painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
  369. painapple_code/static/js/widgets/search-files-widget.js +529 -0
  370. painapple_code/static/js/widgets/skills-widget.js +843 -0
  371. painapple_code/static/js/widgets/snippets-widget.js +479 -0
  372. painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
  373. painapple_code/static/js/widgets/tasks-widget.js +379 -0
  374. painapple_code/static/js/widgets/terminal/connection.js +283 -0
  375. painapple_code/static/js/widgets/terminal/gestures.js +357 -0
  376. painapple_code/static/js/widgets/terminal/init.js +642 -0
  377. painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
  378. painapple_code/static/js/widgets/terminal/osc52.js +213 -0
  379. painapple_code/static/js/widgets/terminal/render.js +159 -0
  380. painapple_code/static/js/widgets/terminal/size.js +54 -0
  381. painapple_code/static/js/widgets/terminal/state.js +211 -0
  382. painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
  383. painapple_code/static/js/widgets/terminal-widget.js +793 -0
  384. painapple_code/static/js/widgets/uploads-widget.js +461 -0
  385. painapple_code/static/js/widgets/zen-widget.js +1001 -0
  386. painapple_code/static/login.html +51 -0
  387. painapple_code/static/manifest.json +73 -0
  388. painapple_code/static/sw.js +293 -0
  389. painapple_code/static/vendor/LICENSES.txt +769 -0
  390. painapple_code/static/vendor/README.md +103 -0
  391. painapple_code/static/vendor/codemirror.js +35 -0
  392. painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
  393. painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
  394. painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
  395. painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
  396. painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
  397. painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
  398. painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
  399. painapple_code/static/vendor/highlight.min.js +1213 -0
  400. painapple_code/static/vendor/marked.min.js +6 -0
  401. painapple_code/static/vendor/purify.min.js +3 -0
  402. painapple_code/static/vendor/xterm-addon-fit.js +2 -0
  403. painapple_code/static/vendor/xterm.css +209 -0
  404. painapple_code/static/vendor/xterm.js +2 -0
  405. painapple_code/static/web-client.html +635 -0
  406. painapple_code/subprocess_registry.py +535 -0
  407. painapple_code/tls_cert.py +79 -0
  408. painapple_code/tools/agents/shadow-git-helper.md +400 -0
  409. painapple_code/tools/excalidraw-to-svg.js +143 -0
  410. painapple_code/tools/install-helpers.sh +148 -0
  411. painapple_code/tools/shadow-git +320 -0
  412. painapple_code/tools/shadow-query +120 -0
  413. painapple_code/tools/vegalite-to-svg.js +79 -0
  414. painapple_code/turn_query.py +766 -0
  415. painapple_code/turn_tracker.py +221 -0
  416. painapple_code/utils/__init__.py +0 -0
  417. painapple_code/utils/agent_cli.py +306 -0
  418. painapple_code/utils/chart.py +103 -0
  419. painapple_code/utils/excalidraw.py +107 -0
  420. painapple_code/utils/file_lock.py +162 -0
  421. painapple_code/utils/file_paths.py +648 -0
  422. painapple_code/utils/generate_icons.py +251 -0
  423. painapple_code/utils/proc.py +205 -0
  424. painapple_code/utils/pty_backend.py +59 -0
  425. painapple_code/utils/pty_posix.py +160 -0
  426. painapple_code/utils/pty_windows.py +245 -0
  427. painapple_code/utils/token_profiles.py +91 -0
  428. painapple_code/viewer_templates.py +657 -0
  429. painapple_code/welcome_search.py +879 -0
  430. painapple_code-1.0.0.dist-info/METADATA +292 -0
  431. painapple_code-1.0.0.dist-info/RECORD +436 -0
  432. painapple_code-1.0.0.dist-info/WHEEL +5 -0
  433. painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
  434. painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
  435. painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
  436. painapple_code-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,74 @@
1
+ """
2
+ Claude Code CLI provider.
3
+
4
+ The reference implementation. Bodies here were moved (not rewritten) out of
5
+ `services/agent_session.py` and `utils/agent_cli.py`, so behavior is
6
+ identical — this is purely the relocation of Claude-specific logic behind the
7
+ `Provider` seam.
8
+
9
+ Claude's stream-json schema *is* the canonical internal event shape, so this
10
+ adapter is a near-identity: it inherits the base `translate_events` and only
11
+ normalizes cumulative cost to per-turn deltas. It's
12
+ assembled from focused mixins (this file is the combiner — see session_store.py
13
+ / shadow_git.py for the same pattern):
14
+
15
+ capabilities.py → _CapabilitiesMixin binary, accounts, models, effort/perm
16
+ vocabularies, fetch_context,
17
+ skill/agent/plugin roots
18
+ launch.py → _LaunchMixin build_command (`claude -p …`)
19
+ translate.py → _TranslateMixin normalize_result (cumulative → delta)
20
+ summary.py → _SummaryMixin rich-commit summary fork (--fork-session)
21
+ errors.py → _ErrorsMixin stderr classification / retry policy
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from painapple_code.providers.base import Provider, Capabilities
27
+ from painapple_code.providers.claude.capabilities import _CapabilitiesMixin
28
+ from painapple_code.providers.claude.launch import _LaunchMixin
29
+ from painapple_code.providers.claude.translate import _TranslateMixin
30
+ from painapple_code.providers.claude.summary import _SummaryMixin
31
+ from painapple_code.providers.claude.errors import _ErrorsMixin
32
+
33
+
34
+ class ClaudeProvider(
35
+ _CapabilitiesMixin,
36
+ _LaunchMixin,
37
+ _TranslateMixin,
38
+ _SummaryMixin,
39
+ _ErrorsMixin,
40
+ Provider,
41
+ ):
42
+ """Adapter for the Claude Code CLI (`claude -p ... --output-format stream-json`).
43
+
44
+ The behaviour lives in the mixins above; this class only carries the static
45
+ identity + capability flags and the MRO that composes them (mixins first, so
46
+ their overrides win over the `Provider` defaults).
47
+ """
48
+
49
+ name = "claude"
50
+ display_name = "Claude Code"
51
+ description = "Anthropic Claude Code CLI — line-protocol driver"
52
+ # Same engine as claude-sdk but the plainer driver (no live controls, no
53
+ # ask-mode cards) — kept out of the picker by default so Claude appears
54
+ # once. Flip it on in Settings → Engines.
55
+ default_enabled = False
56
+ capabilities = Capabilities(
57
+ resume=True,
58
+ fork=True,
59
+ permission_modes=True,
60
+ effort=True,
61
+ thinking_display=True,
62
+ context_command=True,
63
+ cumulative_cost=True,
64
+ rich_commit_summaries=True,
65
+ persistent_process=True, # one `claude -p` process serves many turns
66
+ forward_plain_stderr=True,
67
+ )
68
+
69
+
70
+ # Instances this module contributes to the registry. The package scanner
71
+ # (providers/__init__.py) collects every module's PROVIDERS list, so adding a
72
+ # provider is "drop a file that exports PROVIDERS" — no core edit. (A subpackage
73
+ # is discovered exactly like a flat module.)
74
+ PROVIDERS = [ClaudeProvider()]
@@ -0,0 +1,218 @@
1
+ """Claude provider — declarative surface + customization roots.
2
+
3
+ What the Claude adapter *declares about itself*: binary location, accounts,
4
+ models, effort/permission vocabularies, context fetch, and where its
5
+ skills/agents/plugins live. The `ClaudeProvider` combiner in this package's
6
+ ``__init__`` mixes this in.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Optional
13
+
14
+ from painapple_code.providers.base import PluginBackend
15
+ from painapple_code.utils.agent_cli import get_claude_binary, fetch_context_tokens
16
+
17
+
18
+ class _CapabilitiesMixin:
19
+ """Provider self-description: binary, accounts, vocabularies, roots, context."""
20
+
21
+ # --- binary -----------------------------------------------------------
22
+
23
+ # Settings "CLI path" row + generic engine-path endpoint (shared by both
24
+ # Claude drivers — same binary). The app also owns this engine's model
25
+ # catalog (models.yaml), so Settings renders it editable; per-model
26
+ # show/hide prefs are namespaced under the shared "claude" key so both
27
+ # drivers see the same curation.
28
+ path_config_key = "claude_path"
29
+ default_binary = "claude"
30
+ # "Continue in CLI" quick action → `claude -r <session-id>`.
31
+ cli_resume_template = "claude -r {id}"
32
+ models_editable = True
33
+ models_key = "claude"
34
+ # `claude auth status --json` → {loggedIn, authMethod, email,
35
+ # subscriptionType}; the interactive login flow prints an OAuth URL +
36
+ # paste-back code, which works fine inside a PTY terminal tab.
37
+ auth_status_args = ["auth", "status", "--json"]
38
+ auth_login_args = ["auth", "login"]
39
+
40
+ def binary(self) -> str:
41
+ return get_claude_binary()
42
+
43
+ def binary_not_found_hint(self) -> str:
44
+ return ("Make sure the Claude Code CLI is installed and 'claude' is on "
45
+ "the server's PATH, or set the full path in Settings → Engines. "
46
+ "Install with: curl -fsSL https://claude.ai/install.sh | bash")
47
+
48
+ def summary_model_editable(self) -> bool:
49
+ # Claude's journal model lives in models.yaml (`summary_model`), not
50
+ # a global-config key — override storage, keep the knob.
51
+ return True
52
+
53
+ def get_summary_model_override(self) -> Optional[str]:
54
+ from painapple_code.paths import get_summary_model
55
+ return get_summary_model()
56
+
57
+ def set_summary_model_override(self, value: Optional[str]) -> None:
58
+ # Empty resets to the shipped default — Claude's journal fork always
59
+ # needs a concrete model (`--model` is mandatory on the fork argv).
60
+ from painapple_code import paths
61
+ cleaned = (value or "").strip() or (
62
+ paths.get_default_models_config().get("summary_model")
63
+ or "claude-haiku-4-5")
64
+ paths.save_models_config({
65
+ "selectable": paths.get_selectable_models(),
66
+ "summary_model": cleaned,
67
+ })
68
+
69
+ def parse_auth_status(self, returncode: int, stdout: str, stderr: str) -> dict:
70
+ """Structured status: surface who's logged in and on which plan."""
71
+ try:
72
+ data = json.loads(stdout or "")
73
+ except (ValueError, TypeError):
74
+ data = None
75
+ if not isinstance(data, dict) or "loggedIn" not in data:
76
+ return super().parse_auth_status(returncode, stdout, stderr)
77
+ if not data.get("loggedIn"):
78
+ return {"logged_in": False, "detail": ""}
79
+ bits = [str(b) for b in (data.get("email"), data.get("subscriptionType")) if b]
80
+ return {"logged_in": True, "detail": " · ".join(bits)}
81
+
82
+ # --- accounts / models / effort / permissions -------------------------
83
+
84
+ def accounts(self) -> list[dict]:
85
+ """Claude accounts == token profiles (`CLAUDE_CODE_OAUTH_TOKEN`).
86
+
87
+ Empty id = the ambient login / global-default token. Each named profile
88
+ is a file under ~/.config/painapple-code/tokens/.
89
+ """
90
+ from painapple_code.utils.token_profiles import list_profiles
91
+ accounts = [{"id": "", "label": "Default"}]
92
+ accounts += [{"id": p["name"], "label": p["name"]} for p in list_profiles()]
93
+ return accounts
94
+
95
+ def models(self) -> list[dict]:
96
+ from painapple_code.paths import get_selectable_models
97
+ return get_selectable_models()
98
+
99
+ def effort_levels(self) -> list[str]:
100
+ # The Claude CLI's full --effort scale.
101
+ return ["low", "medium", "high", "xhigh", "max"]
102
+
103
+ def permission_modes(self) -> list[dict]:
104
+ # Claude's native permission modes, in display order — fully self-described
105
+ # here (value/label/desc/color), exactly like every other provider. No
106
+ # Claude permission vocabulary lives anywhere else (strings.yaml, a
107
+ # constant, the frontend); the engine picker and chip read this list.
108
+ return [
109
+ {"value": "plan", "label": "Plan",
110
+ "desc": "Read-only, explore & design", "color": "#8b5cf6"},
111
+ {"value": "dontAsk", "label": "Don't Ask",
112
+ "desc": "Auto-deny unless in allow rules", "color": "#facc15"},
113
+ {"value": "acceptEdits", "label": "Accept Edits",
114
+ "desc": "Auto-approve edits in workspace; deny others", "color": "#fb923c"},
115
+ {"value": "auto", "label": "Auto",
116
+ "desc": "AI classifier reviews each tool call", "color": "#60a5fa"},
117
+ {"value": "bypassPermissions", "label": "YOLO",
118
+ "desc": "Skip all permission checks", "color": "#4ade80"},
119
+ ]
120
+
121
+ def default_permission_mode(self) -> str:
122
+ return "dontAsk"
123
+
124
+ # --- context metering -------------------------------------------------
125
+
126
+ async def fetch_context(
127
+ self,
128
+ cwd: str,
129
+ session_id: Optional[str] = None,
130
+ token_profile: Optional[str] = None,
131
+ model: Optional[str] = None,
132
+ ) -> Optional[dict]:
133
+ return await fetch_context_tokens(
134
+ cwd,
135
+ provider_session_id=session_id,
136
+ token_profile=token_profile,
137
+ model=model,
138
+ )
139
+
140
+ # --- customization roots (skills / agents / plugins) ------------------
141
+
142
+ def skill_roots(self, cwd: str) -> list[dict]:
143
+ from pathlib import Path
144
+ return [
145
+ {"scope": "project", "dir": Path(cwd) / ".claude" / "skills"},
146
+ {"scope": "personal", "dir": Path.home() / ".claude" / "skills"},
147
+ ]
148
+
149
+ def agent_roots(self, cwd: str) -> list[dict]:
150
+ from pathlib import Path
151
+ return [
152
+ {"scope": "project", "dir": Path(cwd) / ".claude" / "agents",
153
+ "fmt": "markdown", "writable": True},
154
+ {"scope": "personal", "dir": Path.home() / ".claude" / "agents",
155
+ "fmt": "markdown", "writable": True},
156
+ ]
157
+
158
+ def plugin_backend(self) -> PluginBackend:
159
+ from pathlib import Path
160
+ return PluginBackend(
161
+ binary=self.binary(),
162
+ list_installed_args=["plugins", "list", "--json"],
163
+ list_all_args=["plugins", "list", "--available", "--json"],
164
+ install_verb=["plugins", "install"],
165
+ uninstall_verb=["plugins", "uninstall"],
166
+ enable_verb=["plugins", "enable"],
167
+ disable_verb=["plugins", "disable"],
168
+ marketplace_root=Path.home() / ".claude" / "plugins" / "marketplaces",
169
+ )
170
+
171
+ def plugin_skill_dirs(self) -> list[dict]:
172
+ """Skill roots from installed Claude plugins (read-only).
173
+
174
+ Claude Code caches every known marketplace under
175
+ ``~/.claude/plugins/marketplaces/`` but only a subset is installed;
176
+ ``installed_plugins.json`` is the authoritative list (keyed
177
+ ``<plugin>@<marketplace>``). We surface only installed plugins' skills.
178
+ """
179
+ from pathlib import Path
180
+ home = Path.home()
181
+ plugins_root = home / ".claude" / "plugins" / "marketplaces"
182
+ if not plugins_root.is_dir():
183
+ return []
184
+ installed = _load_installed_plugin_keys()
185
+ out: list[dict] = []
186
+ for marketplace in sorted(plugins_root.iterdir()):
187
+ plugins_dir = marketplace / "plugins"
188
+ if not plugins_dir.is_dir():
189
+ continue
190
+ for plugin in sorted(plugins_dir.iterdir()):
191
+ if f"{plugin.name}@{marketplace.name}" not in installed:
192
+ continue
193
+ skills_root = plugin / "skills"
194
+ if skills_root.is_dir():
195
+ out.append({
196
+ "label": f"{plugin.name} ({marketplace.name})",
197
+ "dir": skills_root,
198
+ })
199
+ return out
200
+
201
+
202
+ def _load_installed_plugin_keys() -> set:
203
+ """`{plugin@marketplace}` keys from ``~/.claude/plugins/installed_plugins.json``.
204
+
205
+ Empty set if missing/malformed (treat as "nothing installed" rather than
206
+ "show everything", so the picker reflects the user's /plugin install state).
207
+ """
208
+ from pathlib import Path
209
+ path = Path.home() / ".claude" / "plugins" / "installed_plugins.json"
210
+ try:
211
+ with path.open(encoding="utf-8") as f:
212
+ data = json.load(f)
213
+ except (OSError, json.JSONDecodeError):
214
+ return set()
215
+ plugins = data.get("plugins")
216
+ if not isinstance(plugins, dict):
217
+ return set()
218
+ return set(plugins.keys())
@@ -0,0 +1,79 @@
1
+ """Claude provider — stderr classification and retry/termination policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from painapple_code.providers.base import StderrClass
8
+ from painapple_code.utils.agent_cli import NORMAL_TERMINATION_CODES
9
+
10
+ # Patterns that indicate retryable transient API errors (5xx, overloaded, etc.)
11
+ _RETRYABLE_ERROR_PATTERNS = [
12
+ re.compile(r'API Error:\s*5\d{2}\b'),
13
+ re.compile(r'"type":\s*"api_error"'),
14
+ re.compile(r'overloaded', re.IGNORECASE),
15
+ re.compile(r'Internal server error', re.IGNORECASE),
16
+ ]
17
+
18
+ # Usage/rate/session limit. The CLI wording varies by limit kind: "hit your
19
+ # limit", "hit your session limit", "hit your weekly limit", … — one optional
20
+ # word covers them all without matching unrelated "limit" mentions.
21
+ _USAGE_LIMIT_RE = re.compile(r"hit your (?:\w+\s+)?limit", re.IGNORECASE)
22
+
23
+ # Patterns that indicate NON-retryable errors (checked first to avoid retrying)
24
+ _NON_RETRYABLE_PATTERNS = [
25
+ _USAGE_LIMIT_RE,
26
+ re.compile(r'authentication', re.IGNORECASE),
27
+ re.compile(r'invalid.*api.*key', re.IGNORECASE),
28
+ re.compile(r'credit', re.IGNORECASE),
29
+ ]
30
+
31
+ # Patterns in a failed turn's result text that mean "re-login fixes this".
32
+ # Narrower than _NON_RETRYABLE_PATTERNS (no `credit` — a billing problem, not
33
+ # something `claude auth login` resolves).
34
+ _AUTH_ERROR_PATTERNS = [
35
+ re.compile(r'failed to authenticate', re.IGNORECASE),
36
+ re.compile(r'authentication_error', re.IGNORECASE),
37
+ re.compile(r'invalid authentication', re.IGNORECASE),
38
+ re.compile(r'oauth token (?:has )?expired', re.IGNORECASE),
39
+ re.compile(r'invalid.*api.*key', re.IGNORECASE),
40
+ ]
41
+
42
+
43
+ class _ErrorsMixin:
44
+ """Maps Claude stderr/result text to the canonical error classes."""
45
+
46
+ def classify_stderr(self, line: str) -> StderrClass:
47
+ if "No conversation found with session ID" in line:
48
+ return StderrClass.STALE_SESSION
49
+ if "compacting" in line.lower():
50
+ return StderrClass.COMPACTING
51
+ if self.is_retryable_api_error(line):
52
+ return StderrClass.RETRYABLE
53
+ return StderrClass.NONE
54
+
55
+ def is_retryable_api_error(self, text: str) -> bool:
56
+ for pattern in _NON_RETRYABLE_PATTERNS:
57
+ if pattern.search(text):
58
+ return False
59
+ for pattern in _RETRYABLE_ERROR_PATTERNS:
60
+ if pattern.search(text):
61
+ return True
62
+ return False
63
+
64
+ def is_auth_error(self, text: str, api_error_status=None) -> bool:
65
+ if api_error_status == 401:
66
+ return True
67
+ return any(p.search(text) for p in _AUTH_ERROR_PATTERNS)
68
+
69
+ def is_usage_limit(self, text: str) -> bool:
70
+ # Genuine limit messages are one-liners (~50-85 chars: "Error during
71
+ # compaction: You've hit your session limit · resets 4pm (…)"). The
72
+ # length cap keeps the substring match from misfiring on long text
73
+ # that merely QUOTES the phrase — an assistant reply discussing this
74
+ # very bug, pasted logs inside an error string, etc.
75
+ return bool(text) and len(text) <= 200 and bool(_USAGE_LIMIT_RE.search(text))
76
+
77
+ @property
78
+ def normal_termination_codes(self) -> frozenset:
79
+ return NORMAL_TERMINATION_CODES
@@ -0,0 +1,50 @@
1
+ """Claude provider — command building.
2
+
3
+ Turns a `LaunchOptions` into the persistent `claude -p … --output-format
4
+ stream-json` argv (model, effort, permission mode, resume/fork). Mixed into
5
+ `ClaudeProvider`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from painapple_code.providers.base import LaunchOptions
11
+
12
+
13
+ class _LaunchMixin:
14
+ """Builds the `claude -p` command line."""
15
+
16
+ def build_command(self, opts: LaunchOptions) -> list[str]:
17
+ cmd = [
18
+ self.binary(),
19
+ "-p",
20
+ "--input-format", "stream-json",
21
+ "--output-format", "stream-json",
22
+ "--verbose",
23
+ # Opus 4.7 changed the thinking-block default from "summarized"
24
+ # to "omitted" (empty text, signature only). Restore summaries
25
+ # on all models — the flag is a no-op where display was already
26
+ # "summarized". See anthropics/claude-code#8477 comment 4269024077.
27
+ "--thinking-display", "summarized",
28
+ ]
29
+
30
+ if opts.model:
31
+ cmd.extend(["--model", opts.model])
32
+
33
+ if opts.fallback_model:
34
+ cmd.extend(["--fallback-model", opts.fallback_model])
35
+
36
+ # "high" is the CLI default; skip to avoid noise.
37
+ if opts.effort and opts.effort != "high":
38
+ cmd.extend(["--effort", opts.effort])
39
+
40
+ # Fall back to this provider's own default when no mode was resolved.
41
+ cmd.extend(["--permission-mode", opts.permission_mode or self.default_permission_mode()])
42
+
43
+ # Fork from another session (new session with conversation history),
44
+ # else resume an existing one.
45
+ if opts.fork_from_session_id:
46
+ cmd.extend(["--resume", opts.fork_from_session_id, "--fork-session"])
47
+ elif opts.session_id:
48
+ cmd.extend(["--resume", opts.session_id])
49
+
50
+ return cmd
@@ -0,0 +1,89 @@
1
+ """Claude provider — the rich-commit summary fork.
2
+
3
+ Forks the live `claude -p` session with Haiku to write structured commit
4
+ sections, then pulls `structured_output` + usage out of the wrapper JSON. Mixed
5
+ into `ClaudeProvider`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Optional
12
+
13
+ from painapple_code.providers.base import SummaryForkPlan
14
+
15
+
16
+ class _SummaryMixin:
17
+ """Fork the live session with Haiku and read back the structured sections."""
18
+
19
+ def build_summary_fork(
20
+ self,
21
+ *,
22
+ session_id: str,
23
+ fork_prompt: str,
24
+ schema_json: str,
25
+ cwd: str,
26
+ token_profile: Optional[str] = None,
27
+ session_model: Optional[str] = None,
28
+ ) -> Optional[SummaryForkPlan]:
29
+ """Fork the live session with Haiku to write structured commit sections.
30
+
31
+ `--fork-session` branches a throwaway session off the real one, so the
32
+ summary turn never touches the user's conversation; `--json-schema`
33
+ forces the structured shape and `--output-format json` wraps it with
34
+ cost/usage on stdout.
35
+
36
+ The prompt is fed over stdin (bare `-p`, no positional) rather than as an
37
+ argv element: the journey + tools prompt can be large, so this keeps it
38
+ clear of ARG_MAX and out of `ps`. The generic spawner writes
39
+ `stdin_input` before reading stdout.
40
+ """
41
+ from painapple_code.paths import get_summary_model
42
+ from painapple_code.utils.token_profiles import build_env as build_token_env
43
+
44
+ # Always plain haiku — the [1m] variant hangs with --fork-session from
45
+ # opus[1m] sessions (every fork times out at 300s even for 30k context).
46
+ summary_model = get_summary_model()
47
+ return SummaryForkPlan(
48
+ argv=[
49
+ self.binary(),
50
+ "--resume", session_id,
51
+ "--fork-session",
52
+ "--model", summary_model,
53
+ "--tools", "", # disable all tools
54
+ "--no-session-persistence",
55
+ "-p", # print mode; prompt arrives on stdin
56
+ "--output-format", "json", # wrapper JSON carries cost/usage
57
+ "--json-schema", schema_json, # force structured output
58
+ ],
59
+ env=build_token_env(token_profile),
60
+ model=summary_model,
61
+ output_file=None, # structured_output is on stdout
62
+ stdin_input=fork_prompt, # feed the prompt via stdin (not argv)
63
+ )
64
+
65
+ def parse_summary_fork(
66
+ self,
67
+ *,
68
+ plan: SummaryForkPlan,
69
+ returncode: int,
70
+ stdout: bytes,
71
+ stderr: bytes,
72
+ ) -> tuple[Optional[dict], Optional[dict]]:
73
+ """Pull `structured_output` + usage out of the wrapper JSON on stdout."""
74
+ if returncode != 0:
75
+ return None, None
76
+ try:
77
+ result = json.loads(stdout.decode())
78
+ except (json.JSONDecodeError, UnicodeDecodeError):
79
+ return None, None
80
+ structured = result.get("structured_output")
81
+ if not isinstance(structured, dict):
82
+ return None, None
83
+ usage = result.get("usage") or {}
84
+ cost = {
85
+ "cost": result.get("total_cost_usd", 0.0) or 0.0,
86
+ "input_tokens": usage.get("input_tokens", 0),
87
+ "output_tokens": usage.get("output_tokens", 0),
88
+ }
89
+ return structured, cost
@@ -0,0 +1,74 @@
1
+ """Claude provider — wire adaptation + output normalization.
2
+
3
+ Claude's stream-json *is* the canonical internal event shape, so the wire
4
+ methods here (`frame_input`, `session_id_from_event`) are the identity readings
5
+ of that schema, and the base `translate_events` identity is inherited unchanged.
6
+ The one real transform Claude needs is `normalize_result`: the CLI's `result`
7
+ cost/usage fields are cumulative across the persistent `claude -p` process, and
8
+ the server stores them per-turn. Mixed into `ClaudeProvider`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Optional
14
+
15
+ from painapple_code.providers.base import CostState
16
+
17
+ # Cumulative `modelUsage` fields tracked for the per-turn delta conversion.
18
+ _CUM_USAGE_FIELDS = (
19
+ "costUSD", "inputTokens", "outputTokens",
20
+ "cacheReadInputTokens", "cacheCreationInputTokens",
21
+ "webSearchRequests",
22
+ )
23
+
24
+
25
+ class _TranslateMixin:
26
+ """Identity wire methods + per-turn delta conversion for the cumulative result."""
27
+
28
+ def frame_input(self, message: dict) -> dict:
29
+ # Identity: the canonical user-message envelope is exactly what the
30
+ # `claude -p --input-format stream-json` stdin protocol expects
31
+ # ({"type": "user", "message": {...}}).
32
+ return message
33
+
34
+ def session_id_from_event(self, event: dict) -> Optional[str]:
35
+ # Present on every stream-json line the CLI emits.
36
+ return event.get("session_id")
37
+
38
+ def normalize_result(self, msg: dict, state: CostState) -> None:
39
+ """Convert CLI `result` fields from per-process cumulative to per-turn delta.
40
+
41
+ The Claude CLI emits `result` messages whose `total_cost_usd` and
42
+ `modelUsage[*]` fields are running totals across the lifetime of the
43
+ subprocess (across multiple user prompts within one `claude -p`
44
+ invocation). Painapple stores these per-turn, so summing them
45
+ double-counts. This rewrites `msg` in place: cumulative values become
46
+ per-turn deltas (subtracting the previous cumulative seen on this
47
+ subprocess, tracked in `state`).
48
+
49
+ Resilient to out-of-order or negative deltas: if a value somehow drops
50
+ (shouldn't happen within a process, but defensive), treat it as a fresh
51
+ count from zero.
52
+ """
53
+ cum_cost = msg.get("total_cost_usd", 0) or 0.0
54
+ delta_cost = cum_cost - state.cum_cost
55
+ if delta_cost < 0:
56
+ delta_cost = cum_cost
57
+ msg["total_cost_usd"] = delta_cost
58
+ state.cum_cost = cum_cost
59
+
60
+ mu = msg.get("modelUsage") or {}
61
+ if not isinstance(mu, dict):
62
+ return
63
+ prev = state.cum_usage or {}
64
+ new_prev = {}
65
+ for model, info in list(mu.items()):
66
+ if not isinstance(info, dict):
67
+ continue
68
+ prev_info = prev.get(model, {})
69
+ new_prev[model] = {f: info.get(f, 0) or 0 for f in _CUM_USAGE_FIELDS}
70
+ for f in _CUM_USAGE_FIELDS:
71
+ cur = info.get(f, 0) or 0
72
+ d = cur - (prev_info.get(f, 0) or 0)
73
+ info[f] = d if d >= 0 else cur
74
+ state.cum_usage = new_prev