meltygui 0.1.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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,446 @@
1
+ """Anthropic sign-in — the interactive OAuth (PKCE) login that the official
2
+ `ant auth login` performs, run in-process so the Internet Accounts window
3
+ signs an Anthropic (Google / email) account in with one click and no key
4
+ to copy out of the Console.
5
+
6
+ What it produces is an SDK PROFILE, not a key: `configs/<profile>.json` +
7
+ `credentials/<profile>.json` under `$ANTHROPIC_CONFIG_DIR`
8
+ (`~/.config/anthropic`) — the exact files `anthropic.Anthropic(profile=…)`
9
+ reads and REFRESHES itself (refresh_token grant on expiry, rotated tokens
10
+ written back, `anthropic-workspace-id` header from the config), so after
11
+ a sign-in nothing here runs again until Sign out. The login runs as the
12
+ CLI's public OAuth client: a profile is bound to the client that minted
13
+ it (the SDK's refresh sends that client_id), and this way the profile is
14
+ one `ant` / the SDKs share. The studio always names its profile
15
+ explicitly (`Toggles.InternetAccounts.anthropic_profile`, never "default"
16
+ and never the `active_config` pointer), so Claude Code / a bare
17
+ `Anthropic()` elsewhere keep whatever login they have.
18
+
19
+ Flow (`LoginFlow`): bind 127.0.0.1:<ephemeral> → open the Console's
20
+ /oauth/authorize consent page in the SYSTEM browser (Google blocks its
21
+ sign-in inside embedded web views, so a real browser is the path that
22
+ works) → the Console redirects to http://localhost:<port>/callback with
23
+ `code` + `state` → exchange the code at /v1/oauth/token (form-encoded,
24
+ no beta header — the authorization_code grant) → write the profile.
25
+ Everything after `start()` runs on a worker thread; the window reads
26
+ `url` / `done` / `error` and calls `cancel()`.
27
+
28
+ Browser launch goes through copilot.open_url (full-path xdg-open via
29
+ posix_spawn — a fork of the CUDA/GL address space stalls the render
30
+ thread, project_subprocess_fork_stall).
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import base64
35
+ import hashlib
36
+ import html
37
+ import json
38
+ import os
39
+ import secrets
40
+ import threading
41
+ import time
42
+ import urllib.error
43
+ import urllib.parse
44
+ import urllib.request
45
+ from http.server import BaseHTTPRequestHandler, HTTPServer
46
+ from pathlib import Path
47
+
48
+ DEFAULT_BASE_URL = "https://api.anthropic.com"
49
+
50
+
51
+ # ──────────────────────────────────────────────────────────────────────────
52
+ # Profile files (the SDK's on-disk format, `anthropic/lib/credentials`)
53
+ # ──────────────────────────────────────────────────────────────────────────
54
+
55
+ def config_dir() -> Path:
56
+ """`$ANTHROPIC_CONFIG_DIR` or `~/.config/anthropic` — the SDK's rule."""
57
+ env = os.environ.get("ANTHROPIC_CONFIG_DIR")
58
+ return Path(env) if env else Path.home() / ".config" / "anthropic"
59
+
60
+
61
+ def profile_paths(profile: str) -> tuple[Path, Path]:
62
+ """(configs/<profile>.json, credentials/<profile>.json)."""
63
+ _check_profile_name(profile)
64
+ base = config_dir()
65
+ return base / "configs" / f"{profile}.json", base / "credentials" / f"{profile}.json"
66
+
67
+
68
+ def _check_profile_name(profile: str):
69
+ if (not profile or profile in (".", "..") or "/" in profile or "\\" in profile
70
+ or profile.startswith(".")):
71
+ raise ValueError(f"invalid profile name {profile!r}")
72
+
73
+
74
+ def active_profile_present() -> bool:
75
+ """True when a bare `anthropic.Anthropic()` would find a login on its own:
76
+ an `active_config` pointer or a `configs/default.json` (what `ant auth
77
+ login` / Claude Code write). The studio's named profile is NOT this —
78
+ it is passed as `profile=` explicitly."""
79
+ base = config_dir()
80
+ return (base / "active_config").is_file() or (base / "configs" / "default.json").is_file()
81
+
82
+
83
+ def read_profile(profile: str):
84
+ """Who this profile is signed in as, from the credentials file (no
85
+ network): {"profile", "email", "organization", "workspace",
86
+ "expires_at", "refreshable", "path"} — or None when not signed in."""
87
+ try:
88
+ _, credentials_path = profile_paths(profile)
89
+ credentials = json.loads(credentials_path.read_text(encoding="utf-8"))
90
+ except (OSError, ValueError):
91
+ return None
92
+ if not isinstance(credentials, dict) or not credentials.get("access_token"):
93
+ return None
94
+ return {"profile": profile,
95
+ "email": credentials.get("account_email") or "",
96
+ "organization": credentials.get("organization_name") or "",
97
+ "workspace": credentials.get("workspace_name") or "",
98
+ "expires_at": credentials.get("expires_at"),
99
+ "refreshable": bool(credentials.get("refresh_token")),
100
+ "path": credentials_path}
101
+
102
+
103
+ def summary(info) -> str:
104
+ """The status-row text for read_profile(): the account email (the row
105
+ carries no other label — the kind header names the service)."""
106
+ who = info.get("email") or "signed in"
107
+ if not info.get("refreshable"):
108
+ who += " · no refresh"
109
+ return who
110
+
111
+
112
+ def write_profile(profile: str, token: dict, client_id: str,
113
+ base_url: str | None = None, console_url: str | None = None) -> Path:
114
+ """Persist a /v1/oauth/token response as the SDK profile `profile`.
115
+ The config file (non-secret intent: org, workspace, base_url) is kept
116
+ when it already exists for this client — like `ant`, a re-login never
117
+ rewrites it — and the credentials file (0600, dir 0700) is always
118
+ rewritten. Returns the credentials path."""
119
+ config_path, credentials_path = profile_paths(profile)
120
+ organization = token.get("organization") or {}
121
+ account = token.get("account") or {}
122
+ workspace = token.get("workspace") or {}
123
+
124
+ existing = None
125
+ try:
126
+ existing = json.loads(config_path.read_text(encoding="utf-8"))
127
+ except (OSError, ValueError):
128
+ existing = None
129
+ keep = (isinstance(existing, dict)
130
+ and (existing.get("authentication") or {}).get("type") == "user_oauth"
131
+ and (existing.get("authentication") or {}).get("client_id") == client_id)
132
+ if not keep:
133
+ config = {"version": "1.0",
134
+ "authentication": {"type": "user_oauth", "client_id": client_id}}
135
+ if organization.get("uuid"):
136
+ config["organization_id"] = organization["uuid"]
137
+ if workspace.get("id"):
138
+ config["workspace_id"] = workspace["id"]
139
+ if base_url and base_url.rstrip("/") != DEFAULT_BASE_URL:
140
+ config["base_url"] = base_url.rstrip("/")
141
+ if console_url:
142
+ config["authentication"]["console_url"] = console_url.rstrip("/")
143
+ _atomic_write(config_path, config, file_mode=0o644, dir_mode=0o755)
144
+
145
+ try:
146
+ expires_in = int(token.get("expires_in") or 3600)
147
+ except (TypeError, ValueError):
148
+ expires_in = 3600
149
+ credentials = {"version": "1.0", "type": "oauth_token",
150
+ "access_token": token["access_token"],
151
+ "expires_at": int(time.time()) + expires_in}
152
+ if token.get("refresh_token"):
153
+ credentials["refresh_token"] = token["refresh_token"]
154
+ for key, value in (("scope", token.get("scope")),
155
+ ("organization_uuid", organization.get("uuid")),
156
+ ("organization_name", organization.get("name")),
157
+ ("account_email", account.get("email_address")),
158
+ ("workspace_id", workspace.get("id")),
159
+ ("workspace_name", workspace.get("name"))):
160
+ if value:
161
+ credentials[key] = value
162
+ _atomic_write(credentials_path, credentials, file_mode=0o600, dir_mode=0o700)
163
+ return credentials_path
164
+
165
+
166
+ def sign_out(profile: str) -> bool:
167
+ """Remove the profile's credentials (the config — org/workspace intent —
168
+ stays, so a re-login lands in the same org without the picker; same as
169
+ `ant auth logout`). True when a credentials file was removed."""
170
+ _, credentials_path = profile_paths(profile)
171
+ try:
172
+ credentials_path.unlink()
173
+ return True
174
+ except FileNotFoundError:
175
+ return False
176
+
177
+
178
+ def _atomic_write(path: Path, data: dict, file_mode: int, dir_mode: int):
179
+ path.parent.mkdir(parents=True, exist_ok=True)
180
+ try:
181
+ os.chmod(path.parent, dir_mode)
182
+ except OSError:
183
+ pass
184
+ tmp = path.with_name(path.name + ".tmp")
185
+ with open(tmp, "w", encoding="utf-8") as file:
186
+ file.write(json.dumps(data, indent=2))
187
+ file.write("\n")
188
+ os.chmod(tmp, file_mode)
189
+ os.replace(tmp, path)
190
+
191
+
192
+ # ──────────────────────────────────────────────────────────────────────────
193
+ # OAuth helpers (RFC 7636 PKCE, authorization_code grant)
194
+ # ──────────────────────────────────────────────────────────────────────────
195
+
196
+ def random_urlsafe(n_bytes: int) -> str:
197
+ return base64.urlsafe_b64encode(secrets.token_bytes(n_bytes)).decode().rstrip("=")
198
+
199
+
200
+ def pkce_challenge(verifier: str) -> str:
201
+ digest = hashlib.sha256(verifier.encode("ascii")).digest()
202
+ return base64.urlsafe_b64encode(digest).decode().rstrip("=")
203
+
204
+
205
+ def build_authorize_url(console_url: str, client_id: str, redirect_uri: str, scope: str,
206
+ state: str, challenge: str,
207
+ organization_id: str | None = None, workspace_id: str | None = None) -> str:
208
+ """The Console consent page. `organization_id` (`?orgUUID=`) makes the
209
+ Console skip its org picker when the signed-in account is a member;
210
+ `workspace_id` skips the workspace picker. Either omitted → picker."""
211
+ params = [("client_id", client_id), ("redirect_uri", redirect_uri), ("response_type", "code"),
212
+ ("scope", scope), ("state", state), ("code_challenge", challenge),
213
+ ("code_challenge_method", "S256")]
214
+ if workspace_id:
215
+ params.append(("workspace_id", workspace_id))
216
+ if organization_id:
217
+ params.append(("orgUUID", organization_id))
218
+ return f"{console_url.rstrip('/')}/oauth/authorize?{urllib.parse.urlencode(params)}"
219
+
220
+
221
+ def exchange_code(base_url: str, client_id: str, code: str, verifier: str, redirect_uri: str,
222
+ state: str, timeout_s: float = 30.0) -> dict:
223
+ """Redeem the authorization code at /v1/oauth/token. Form-encoded and
224
+ WITHOUT an anthropic-beta header — that is the authorization_code
225
+ grant's route (`state` is required on this leg too: it is the bound
226
+ CSRF check across both legs)."""
227
+ from meltygui.completion.providers.anthropic_requests import notify_request
228
+ notify_request("POST /v1/oauth/token", "browser sign-in code exchange")
229
+ body = urllib.parse.urlencode({"grant_type": "authorization_code", "code": code,
230
+ "code_verifier": verifier, "client_id": client_id,
231
+ "redirect_uri": redirect_uri, "state": state}).encode()
232
+ request = urllib.request.Request(
233
+ f"{base_url.rstrip('/')}/v1/oauth/token", data=body, method="POST",
234
+ headers={"Content-Type": "application/x-www-form-urlencoded",
235
+ "Accept": "application/json", "User-Agent": "latent-descent"})
236
+ try:
237
+ with urllib.request.urlopen(request, timeout=timeout_s) as response:
238
+ raw = response.read()
239
+ except urllib.error.HTTPError as error:
240
+ detail = error.read().decode("utf-8", "replace").strip()
241
+ request_id = error.headers.get("request-id", "") if error.headers else ""
242
+ raise RuntimeError(f"token endpoint returned {error.code}"
243
+ + (f" (request_id={request_id})" if request_id else "")
244
+ + (f": {detail[:200]}" if detail else "")) from None
245
+ except urllib.error.URLError as error:
246
+ raise RuntimeError(f"token endpoint unreachable: {error.reason}") from None
247
+ try:
248
+ payload = json.loads(raw.decode("utf-8"))
249
+ except ValueError:
250
+ raise RuntimeError("token endpoint returned a non-JSON response") from None
251
+ if not isinstance(payload, dict) or not payload.get("access_token"):
252
+ raise RuntimeError("token endpoint returned no access_token")
253
+ return payload
254
+
255
+
256
+ # ──────────────────────────────────────────────────────────────────────────
257
+ # The interactive flow
258
+ # ──────────────────────────────────────────────────────────────────────────
259
+
260
+ class _CallbackHandler(BaseHTTPRequestHandler):
261
+ """The loopback redirect target. Only /callback counts (a favicon or a
262
+ stray local request must not read as a failed sign-in); `state` gates
263
+ the code; the FIRST result wins."""
264
+
265
+ def log_message(self, *args): # keep the server's stdout clean
266
+ pass
267
+
268
+ def do_GET(self):
269
+ flow = self.server.flow
270
+ parsed = urllib.parse.urlsplit(self.path)
271
+ if parsed.path != "/callback":
272
+ self._page(404, "Not found", "")
273
+ return
274
+ query = urllib.parse.parse_qs(parsed.query)
275
+
276
+ def param(name):
277
+ return (query.get(name) or [""])[0]
278
+
279
+ if param("error"):
280
+ flow._deliver(error=f"authorization denied: {param('error')}: {param('error_description')}")
281
+ self._page(400, "✗ Sign-in failed", f"{param('error')}: {param('error_description')}")
282
+ elif param("state") != flow.state:
283
+ flow._deliver(error="state mismatch (possible CSRF) — start the sign-in again")
284
+ self._page(400, "✗ State mismatch",
285
+ "The callback state did not match this sign-in attempt. "
286
+ "Do not retry from this browser session.")
287
+ elif not param("code"):
288
+ flow._deliver(error="the authorization callback carried no code")
289
+ self._page(400, "✗ Missing code", "The authorization callback did not include a code.")
290
+ else:
291
+ flow._deliver(code=param("code"))
292
+ self._page(200, "✓ Signed in", "You can close this tab and return to Latent Descent.")
293
+
294
+ def _page(self, status, heading, body):
295
+ content = (f'<html><body style="font-family:system-ui;text-align:center;padding:4em">'
296
+ f'<h2>{html.escape(heading)}</h2><p>{html.escape(body)}</p></body></html>').encode()
297
+ self.send_response(status)
298
+ self.send_header("Content-Type", "text/html; charset=utf-8")
299
+ self.send_header("Content-Length", str(len(content)))
300
+ self.end_headers()
301
+ self.wfile.write(content)
302
+
303
+
304
+ class LoginFlow:
305
+ """One browser sign-in. `start()` binds the loopback port, builds the
306
+ consent URL (`url` — the window's Copy link / Open browser), opens the
307
+ browser and returns; a worker then waits for the redirect, exchanges
308
+ the code and writes the profile. Read `done` / `error` / `result`
309
+ (read_profile() of the new login); `on_change(flow)` fires from the
310
+ worker on every transition. `cancel()` ends the wait within
311
+ `poll_s`."""
312
+
313
+ def __init__(self, profile: str, *, client_id: str | None = None, scope: str | None = None,
314
+ console_url: str | None = None, base_url: str | None = None,
315
+ organization_id: str | None = None, workspace_id: str | None = None,
316
+ open_browser: bool = True, timeout_s: float | None = None, on_change=None):
317
+ from meltygui.core.runtime.toggles import Toggles
318
+ self.profile = profile
319
+ self.client_id = client_id or Toggles.InternetAccounts.anthropic_oauth_client_id
320
+ self.scope = scope or Toggles.InternetAccounts.anthropic_oauth_scope
321
+ self.console_url = (console_url or Toggles.InternetAccounts.anthropic_console_url).rstrip("/")
322
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
323
+ self.organization_id = organization_id
324
+ self.workspace_id = workspace_id
325
+ self.open_browser = open_browser
326
+ self.timeout_s = (Toggles.InternetAccounts.anthropic_login_timeout_s
327
+ if timeout_s is None else timeout_s)
328
+ self.on_change = on_change
329
+ # How often the worker re-checks cancel / timeout between redirects.
330
+ self.poll_s = 0.5
331
+
332
+ self.verifier = random_urlsafe(48)
333
+ self.state = random_urlsafe(24)
334
+ self.redirect_uri = None
335
+ self.url = None
336
+ self.error = None
337
+ self.result = None
338
+ self.done = False
339
+ self.started_at = None
340
+ self._popup = None
341
+ self._server = None
342
+ self._thread = None
343
+ self._code = None
344
+ self._callback_error = None
345
+ self._got = threading.Event()
346
+ self._cancelled = threading.Event()
347
+
348
+ # -- public -----------------------------------------------------------
349
+
350
+ def start(self) -> str:
351
+ if self.organization_id is None:
352
+ self.organization_id = self._stored_organization_id()
353
+ self._server = HTTPServer(("127.0.0.1", 0), _CallbackHandler)
354
+ self._server.flow = self
355
+ self._server.timeout = self.poll_s
356
+ port = self._server.server_address[1]
357
+ # `localhost` in the redirect (the callback form), bound on 127.0.0.1 -
358
+ # the same pairing the CLI uses.
359
+ self.redirect_uri = f"http://localhost:{port}/callback"
360
+ self.url = build_authorize_url(self.console_url, self.client_id, self.redirect_uri,
361
+ self.scope, self.state, pkce_challenge(self.verifier),
362
+ organization_id=self.organization_id,
363
+ workspace_id=self.workspace_id)
364
+ self.started_at = time.monotonic()
365
+ self._thread = threading.Thread(target=self._run, daemon=True,
366
+ name=f"anthropic-login-{self.profile}")
367
+ self._thread.start()
368
+ if self.open_browser:
369
+ self.open_in_browser()
370
+ return self.url
371
+
372
+ def open_in_browser(self) -> bool:
373
+ if not self.url:
374
+ return False
375
+ import meltygui.completion.providers.oauth_popup as oauth_popup
376
+ popup = oauth_popup.open_auth_popup(self.url) # falls back to xdg-open itself
377
+ if popup is not None:
378
+ self._popup = popup
379
+ return True
380
+
381
+ def cancel(self):
382
+ self._cancelled.set()
383
+
384
+ @property
385
+ def alive(self) -> bool:
386
+ return self._thread is not None and not self.done
387
+
388
+ # -- worker -----------------------------------------------------------
389
+
390
+ def _deliver(self, code=None, error=None):
391
+ if self._got.is_set():
392
+ return # first request wins (browser prefetch, reloads)
393
+ self._code, self._callback_error = code, error
394
+ self._got.set()
395
+
396
+ def _run(self):
397
+ deadline = time.monotonic() + self.timeout_s
398
+ try:
399
+ while (not self._got.is_set() and not self._cancelled.is_set()
400
+ and time.monotonic() < deadline):
401
+ self._server.handle_request()
402
+ except Exception as error:
403
+ self._callback_error = f"callback listener failed: {error}"
404
+ self._got.set()
405
+ finally:
406
+ try:
407
+ self._server.server_close()
408
+ except Exception:
409
+ pass
410
+ if self._cancelled.is_set():
411
+ self.error = "sign-in cancelled"
412
+ elif not self._got.is_set():
413
+ self.error = "timed out waiting for the browser sign-in"
414
+ elif self._callback_error:
415
+ self.error = self._callback_error
416
+ else:
417
+ try:
418
+ token = exchange_code(self.base_url, self.client_id, self._code, self.verifier,
419
+ self.redirect_uri, self.state)
420
+ write_profile(self.profile, token, self.client_id,
421
+ base_url=self.base_url, console_url=self.console_url)
422
+ self.result = read_profile(self.profile)
423
+ except Exception as error:
424
+ self.error = f"sign-in failed: {error}"
425
+ self.done = True
426
+ if self._popup is not None:
427
+ self._popup.close() # the flow is over - take the popup with it
428
+ self._notify()
429
+
430
+ def _notify(self):
431
+ if self.on_change is not None:
432
+ try:
433
+ self.on_change(self)
434
+ except Exception:
435
+ pass
436
+
437
+ def _stored_organization_id(self):
438
+ """The org a previous login of this profile bound (config file) —
439
+ passed as the Console's ?orgUUID hint so a re-login skips the org
440
+ picker, like `ant`."""
441
+ try:
442
+ config_path, _ = profile_paths(self.profile)
443
+ config = json.loads(config_path.read_text(encoding="utf-8"))
444
+ return config.get("organization_id") or None
445
+ except (OSError, ValueError, AttributeError):
446
+ return None
@@ -0,0 +1,69 @@
1
+ """Every Anthropic API request the studio makes announces itself — a toast
2
+ in the notifications overlay (tag "anthropic"; repeats while visible
3
+ coalesce into a count) plus a console line — so "who is hitting the API"
4
+ is always visible (born from an /api/oauth/usage rate-limit hunt, 08-25).
5
+ Gate: Toggles.InternetAccounts.notify_requests.
6
+
7
+ Two hooks cover the studio's traffic:
8
+ - `notify_request(what, detail=)` — hand-written call sites: the usage
9
+ fetch, the browser sign-in's token exchange, launching Claude Code's
10
+ `claude auth login` (that one is Claude Code's own traffic, labelled so).
11
+ - `sdk_middleware()` — anthropic SDK middleware for
12
+ `Anthropic(middleware=[…])` (ClaudeSession and the Test button pass it):
13
+ the chain runs once per HTTP ATTEMPT inside the SDK's retry loop, so FIM
14
+ completions, Test, AND every retry announce with method + path, and a
15
+ non-2xx attempt is called out with its status ("429 · rate limited").
16
+
17
+ Known gap: the SDK's own profile token refresh (anthropic/lib/credentials,
18
+ its private httpx client) doesn't run through client middleware.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import time
23
+
24
+
25
+ def enabled() -> bool:
26
+ from meltygui.core.runtime.toggles import Toggles
27
+ return bool(Toggles.InternetAccounts.notify_requests)
28
+
29
+
30
+ def notify_request(what, detail=""):
31
+ """One announced request: console line + overlay toast (never raises,
32
+ never imports the SDK)."""
33
+ if not enabled():
34
+ return
35
+ text = what + (f" · {detail}" if detail else "")
36
+ print(f"[anthropic] {time.strftime('%H:%M:%S')} {text}")
37
+ try:
38
+ from meltygui.core.diagnostics.notifications import notify
39
+ notify(text, tint=(0.85, 0.55, 0.35, 1.0), tag="anthropic")
40
+ except Exception:
41
+ pass
42
+
43
+
44
+ _sdk_middleware = None
45
+
46
+
47
+ def sdk_middleware():
48
+ """The (cached) middleware instance for `anthropic.Anthropic(middleware=
49
+ [sdk_middleware()])`. Built lazily so importing this module never pays
50
+ the anthropic import."""
51
+ global _sdk_middleware
52
+ if _sdk_middleware is None:
53
+ from anthropic import Middleware
54
+
55
+ class RequestNotifier(Middleware):
56
+ def handle(self, request, call_next):
57
+ url = getattr(request, "url", None)
58
+ path = getattr(url, "path", "") or str(url or "?")
59
+ method = getattr(request, "method", "?")
60
+ notify_request(f"{method} {path}")
61
+ response = call_next(request)
62
+ status = getattr(response, "status_code", None)
63
+ if status is not None and status >= 400:
64
+ notify_request(f"{method} {path} → {status}",
65
+ "rate limited" if status == 429 else "")
66
+ return response
67
+
68
+ _sdk_middleware = RequestNotifier()
69
+ return _sdk_middleware