pythinker-code 2.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 (731) hide show
  1. pythinker_code/CHANGELOG.md +16 -0
  2. pythinker_code/__init__.py +0 -0
  3. pythinker_code/__main__.py +92 -0
  4. pythinker_code/acp/AGENTS.md +93 -0
  5. pythinker_code/acp/__init__.py +13 -0
  6. pythinker_code/acp/convert.py +128 -0
  7. pythinker_code/acp/host.py +298 -0
  8. pythinker_code/acp/mcp.py +46 -0
  9. pythinker_code/acp/server.py +497 -0
  10. pythinker_code/acp/session.py +496 -0
  11. pythinker_code/acp/tools.py +167 -0
  12. pythinker_code/acp/types.py +13 -0
  13. pythinker_code/acp/version.py +45 -0
  14. pythinker_code/agents/default/agent.yaml +36 -0
  15. pythinker_code/agents/default/coder.yaml +25 -0
  16. pythinker_code/agents/default/explore.yaml +46 -0
  17. pythinker_code/agents/default/plan.yaml +30 -0
  18. pythinker_code/agents/default/system.md +164 -0
  19. pythinker_code/agents/okabe/agent.yaml +22 -0
  20. pythinker_code/agentspec.py +163 -0
  21. pythinker_code/app.py +820 -0
  22. pythinker_code/approval_runtime/__init__.py +29 -0
  23. pythinker_code/approval_runtime/models.py +42 -0
  24. pythinker_code/approval_runtime/runtime.py +235 -0
  25. pythinker_code/auth/__init__.py +25 -0
  26. pythinker_code/auth/anthropic_direct.py +207 -0
  27. pythinker_code/auth/deepseek.py +192 -0
  28. pythinker_code/auth/lm_studio.py +418 -0
  29. pythinker_code/auth/minimax.py +203 -0
  30. pythinker_code/auth/oauth.py +1122 -0
  31. pythinker_code/auth/ollama.py +293 -0
  32. pythinker_code/auth/openai.py +771 -0
  33. pythinker_code/auth/opencode_go.py +203 -0
  34. pythinker_code/auth/openrouter.py +225 -0
  35. pythinker_code/auth/platforms.py +466 -0
  36. pythinker_code/background/__init__.py +36 -0
  37. pythinker_code/background/agent_runner.py +231 -0
  38. pythinker_code/background/ids.py +19 -0
  39. pythinker_code/background/manager.py +650 -0
  40. pythinker_code/background/models.py +105 -0
  41. pythinker_code/background/store.py +237 -0
  42. pythinker_code/background/summary.py +66 -0
  43. pythinker_code/background/worker.py +209 -0
  44. pythinker_code/cli/__init__.py +1326 -0
  45. pythinker_code/cli/__main__.py +19 -0
  46. pythinker_code/cli/_lazy_group.py +238 -0
  47. pythinker_code/cli/export.py +322 -0
  48. pythinker_code/cli/info.py +62 -0
  49. pythinker_code/cli/mcp.py +349 -0
  50. pythinker_code/cli/plugin.py +351 -0
  51. pythinker_code/cli/toad.py +74 -0
  52. pythinker_code/cli/vis.py +38 -0
  53. pythinker_code/cli/web.py +80 -0
  54. pythinker_code/config.py +453 -0
  55. pythinker_code/constant.py +33 -0
  56. pythinker_code/exception.py +43 -0
  57. pythinker_code/hooks/__init__.py +4 -0
  58. pythinker_code/hooks/config.py +34 -0
  59. pythinker_code/hooks/engine.py +371 -0
  60. pythinker_code/hooks/events.py +190 -0
  61. pythinker_code/hooks/runner.py +89 -0
  62. pythinker_code/llm.py +412 -0
  63. pythinker_code/metadata.py +79 -0
  64. pythinker_code/notifications/__init__.py +33 -0
  65. pythinker_code/notifications/llm.py +77 -0
  66. pythinker_code/notifications/manager.py +145 -0
  67. pythinker_code/notifications/models.py +50 -0
  68. pythinker_code/notifications/notifier.py +41 -0
  69. pythinker_code/notifications/store.py +118 -0
  70. pythinker_code/notifications/wire.py +21 -0
  71. pythinker_code/plugin/__init__.py +124 -0
  72. pythinker_code/plugin/manager.py +153 -0
  73. pythinker_code/plugin/tool.py +173 -0
  74. pythinker_code/prompts/__init__.py +6 -0
  75. pythinker_code/prompts/compact.md +73 -0
  76. pythinker_code/prompts/init.md +21 -0
  77. pythinker_code/py.typed +0 -0
  78. pythinker_code/session.py +319 -0
  79. pythinker_code/session_fork.py +325 -0
  80. pythinker_code/session_state.py +132 -0
  81. pythinker_code/share.py +14 -0
  82. pythinker_code/skill/__init__.py +727 -0
  83. pythinker_code/skill/flow/__init__.py +99 -0
  84. pythinker_code/skill/flow/d2.py +482 -0
  85. pythinker_code/skill/flow/mermaid.py +266 -0
  86. pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
  87. pythinker_code/skills/skill-creator/SKILL.md +367 -0
  88. pythinker_code/soul/__init__.py +304 -0
  89. pythinker_code/soul/agent.py +520 -0
  90. pythinker_code/soul/approval.py +267 -0
  91. pythinker_code/soul/btw.py +214 -0
  92. pythinker_code/soul/compaction.py +189 -0
  93. pythinker_code/soul/context.py +339 -0
  94. pythinker_code/soul/denwarenji.py +39 -0
  95. pythinker_code/soul/dynamic_injection.py +84 -0
  96. pythinker_code/soul/dynamic_injections/__init__.py +0 -0
  97. pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
  98. pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
  99. pythinker_code/soul/message.py +92 -0
  100. pythinker_code/soul/pythinkersoul.py +1613 -0
  101. pythinker_code/soul/slash.py +340 -0
  102. pythinker_code/soul/toolset.py +788 -0
  103. pythinker_code/subagents/__init__.py +21 -0
  104. pythinker_code/subagents/builder.py +42 -0
  105. pythinker_code/subagents/core.py +86 -0
  106. pythinker_code/subagents/git_context.py +172 -0
  107. pythinker_code/subagents/models.py +54 -0
  108. pythinker_code/subagents/output.py +71 -0
  109. pythinker_code/subagents/registry.py +28 -0
  110. pythinker_code/subagents/runner.py +428 -0
  111. pythinker_code/subagents/store.py +196 -0
  112. pythinker_code/telemetry/__init__.py +211 -0
  113. pythinker_code/telemetry/config.py +54 -0
  114. pythinker_code/telemetry/crash.py +157 -0
  115. pythinker_code/telemetry/metrics.py +208 -0
  116. pythinker_code/telemetry/otel.py +240 -0
  117. pythinker_code/telemetry/sentry.py +167 -0
  118. pythinker_code/telemetry/sink.py +189 -0
  119. pythinker_code/tools/AGENTS.md +6 -0
  120. pythinker_code/tools/__init__.py +105 -0
  121. pythinker_code/tools/agent/__init__.py +277 -0
  122. pythinker_code/tools/agent/description.md +41 -0
  123. pythinker_code/tools/ask_user/__init__.py +159 -0
  124. pythinker_code/tools/ask_user/description.md +19 -0
  125. pythinker_code/tools/background/__init__.py +318 -0
  126. pythinker_code/tools/background/list.md +10 -0
  127. pythinker_code/tools/background/output.md +11 -0
  128. pythinker_code/tools/background/stop.md +8 -0
  129. pythinker_code/tools/display.py +46 -0
  130. pythinker_code/tools/dmail/__init__.py +38 -0
  131. pythinker_code/tools/dmail/dmail.md +17 -0
  132. pythinker_code/tools/file/__init__.py +30 -0
  133. pythinker_code/tools/file/glob.md +17 -0
  134. pythinker_code/tools/file/glob.py +160 -0
  135. pythinker_code/tools/file/grep.md +6 -0
  136. pythinker_code/tools/file/grep_local.py +589 -0
  137. pythinker_code/tools/file/plan_mode.py +45 -0
  138. pythinker_code/tools/file/read.md +16 -0
  139. pythinker_code/tools/file/read.py +300 -0
  140. pythinker_code/tools/file/read_media.md +24 -0
  141. pythinker_code/tools/file/read_media.py +217 -0
  142. pythinker_code/tools/file/replace.md +7 -0
  143. pythinker_code/tools/file/replace.py +195 -0
  144. pythinker_code/tools/file/utils.py +257 -0
  145. pythinker_code/tools/file/write.md +5 -0
  146. pythinker_code/tools/file/write.py +177 -0
  147. pythinker_code/tools/plan/__init__.py +327 -0
  148. pythinker_code/tools/plan/description.md +29 -0
  149. pythinker_code/tools/plan/enter.py +190 -0
  150. pythinker_code/tools/plan/enter_description.md +35 -0
  151. pythinker_code/tools/plan/heroes.py +277 -0
  152. pythinker_code/tools/shell/__init__.py +253 -0
  153. pythinker_code/tools/shell/bash.md +35 -0
  154. pythinker_code/tools/shell/powershell.md +30 -0
  155. pythinker_code/tools/test.py +55 -0
  156. pythinker_code/tools/think/__init__.py +21 -0
  157. pythinker_code/tools/think/think.md +1 -0
  158. pythinker_code/tools/todo/__init__.py +168 -0
  159. pythinker_code/tools/todo/set_todo_list.md +23 -0
  160. pythinker_code/tools/utils.py +199 -0
  161. pythinker_code/tools/web/__init__.py +4 -0
  162. pythinker_code/tools/web/fetch.md +1 -0
  163. pythinker_code/tools/web/fetch.py +189 -0
  164. pythinker_code/tools/web/search.md +1 -0
  165. pythinker_code/tools/web/search.py +163 -0
  166. pythinker_code/ui/__init__.py +0 -0
  167. pythinker_code/ui/acp/__init__.py +99 -0
  168. pythinker_code/ui/print/__init__.py +474 -0
  169. pythinker_code/ui/print/visualize.py +185 -0
  170. pythinker_code/ui/shell/__init__.py +1696 -0
  171. pythinker_code/ui/shell/console.py +109 -0
  172. pythinker_code/ui/shell/debug.py +190 -0
  173. pythinker_code/ui/shell/echo.py +17 -0
  174. pythinker_code/ui/shell/export_import.py +117 -0
  175. pythinker_code/ui/shell/keyboard.py +300 -0
  176. pythinker_code/ui/shell/mcp_status.py +113 -0
  177. pythinker_code/ui/shell/model_picker.py +318 -0
  178. pythinker_code/ui/shell/oauth.py +272 -0
  179. pythinker_code/ui/shell/placeholders.py +531 -0
  180. pythinker_code/ui/shell/prompt.py +2278 -0
  181. pythinker_code/ui/shell/replay.py +215 -0
  182. pythinker_code/ui/shell/session_picker.py +227 -0
  183. pythinker_code/ui/shell/setup.py +212 -0
  184. pythinker_code/ui/shell/slash.py +898 -0
  185. pythinker_code/ui/shell/startup.py +32 -0
  186. pythinker_code/ui/shell/task_browser.py +486 -0
  187. pythinker_code/ui/shell/update.py +350 -0
  188. pythinker_code/ui/shell/usage.py +291 -0
  189. pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
  190. pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
  191. pythinker_code/ui/shell/usage_adapters/base.py +72 -0
  192. pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
  193. pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
  194. pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
  195. pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
  196. pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
  197. pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
  198. pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
  199. pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
  200. pythinker_code/ui/shell/usage_render.py +150 -0
  201. pythinker_code/ui/shell/visualize/__init__.py +165 -0
  202. pythinker_code/ui/shell/visualize/_approval_panel.py +505 -0
  203. pythinker_code/ui/shell/visualize/_blocks.py +629 -0
  204. pythinker_code/ui/shell/visualize/_btw_panel.py +224 -0
  205. pythinker_code/ui/shell/visualize/_input_router.py +48 -0
  206. pythinker_code/ui/shell/visualize/_interactive.py +523 -0
  207. pythinker_code/ui/shell/visualize/_live_view.py +826 -0
  208. pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
  209. pythinker_code/ui/theme.py +241 -0
  210. pythinker_code/usage_ratelimit_cache.py +175 -0
  211. pythinker_code/utils/__init__.py +0 -0
  212. pythinker_code/utils/aiohttp.py +24 -0
  213. pythinker_code/utils/aioqueue.py +72 -0
  214. pythinker_code/utils/broadcast.py +37 -0
  215. pythinker_code/utils/changelog.py +108 -0
  216. pythinker_code/utils/clipboard.py +246 -0
  217. pythinker_code/utils/datetime.py +64 -0
  218. pythinker_code/utils/diff.py +135 -0
  219. pythinker_code/utils/editor.py +91 -0
  220. pythinker_code/utils/environment.py +73 -0
  221. pythinker_code/utils/envvar.py +22 -0
  222. pythinker_code/utils/export.py +696 -0
  223. pythinker_code/utils/file_filter.py +375 -0
  224. pythinker_code/utils/frontmatter.py +70 -0
  225. pythinker_code/utils/io.py +27 -0
  226. pythinker_code/utils/logging.py +146 -0
  227. pythinker_code/utils/media_tags.py +29 -0
  228. pythinker_code/utils/message.py +24 -0
  229. pythinker_code/utils/path.py +199 -0
  230. pythinker_code/utils/proctitle.py +33 -0
  231. pythinker_code/utils/proxy.py +31 -0
  232. pythinker_code/utils/pyinstaller.py +45 -0
  233. pythinker_code/utils/rich/__init__.py +33 -0
  234. pythinker_code/utils/rich/columns.py +99 -0
  235. pythinker_code/utils/rich/diff_render.py +481 -0
  236. pythinker_code/utils/rich/markdown.py +900 -0
  237. pythinker_code/utils/rich/markdown_sample.md +108 -0
  238. pythinker_code/utils/rich/markdown_sample_short.md +2 -0
  239. pythinker_code/utils/rich/syntax.py +114 -0
  240. pythinker_code/utils/sensitive.py +54 -0
  241. pythinker_code/utils/server.py +121 -0
  242. pythinker_code/utils/signals.py +43 -0
  243. pythinker_code/utils/slashcmd.py +124 -0
  244. pythinker_code/utils/string.py +41 -0
  245. pythinker_code/utils/subprocess_env.py +73 -0
  246. pythinker_code/utils/term.py +168 -0
  247. pythinker_code/utils/typing.py +20 -0
  248. pythinker_code/vis/__init__.py +0 -0
  249. pythinker_code/vis/api/__init__.py +5 -0
  250. pythinker_code/vis/api/sessions.py +687 -0
  251. pythinker_code/vis/api/statistics.py +209 -0
  252. pythinker_code/vis/api/system.py +19 -0
  253. pythinker_code/vis/app.py +175 -0
  254. pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-D2MTYyJz.js +1 -0
  255. pythinker_code/vis/static/assets/index-CezafTt_.js +185 -0
  256. pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
  257. pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  258. pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  259. pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  260. pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  261. pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  262. pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  263. pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  264. pythinker_code/vis/static/index.html +17 -0
  265. pythinker_code/web/__init__.py +5 -0
  266. pythinker_code/web/api/__init__.py +15 -0
  267. pythinker_code/web/api/config.py +208 -0
  268. pythinker_code/web/api/open_in.py +199 -0
  269. pythinker_code/web/api/sessions.py +1225 -0
  270. pythinker_code/web/app.py +451 -0
  271. pythinker_code/web/auth.py +191 -0
  272. pythinker_code/web/models.py +98 -0
  273. pythinker_code/web/runner/__init__.py +5 -0
  274. pythinker_code/web/runner/messages.py +57 -0
  275. pythinker_code/web/runner/process.py +754 -0
  276. pythinker_code/web/runner/worker.py +97 -0
  277. pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  278. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  279. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  280. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  281. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  282. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  283. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  284. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  285. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  286. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  287. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  288. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  289. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  290. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  291. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  292. pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  293. pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  294. pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  295. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  296. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  297. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  298. pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  299. pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  300. pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  301. pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  302. pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  303. pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  304. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  305. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  306. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  307. pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  308. pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  309. pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  310. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  311. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  312. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  313. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  314. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  315. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  316. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  317. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  318. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  319. pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  320. pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  321. pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  322. pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  323. pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  324. pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  325. pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  326. pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  327. pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  328. pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  329. pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  330. pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  331. pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  332. pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  333. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  334. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  335. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  336. pythinker_code/web/static/assets/_baseUniq--dyU3g5v.js +1 -0
  337. pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
  338. pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
  339. pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
  340. pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
  341. pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
  342. pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
  343. pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
  344. pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
  345. pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
  346. pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
  347. pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
  348. pythinker_code/web/static/assets/arc-DkMjLpYa.js +1 -0
  349. pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-CHWVaGo9.js +36 -0
  350. pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
  351. pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
  352. pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
  353. pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  354. pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
  355. pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
  356. pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
  357. pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
  358. pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
  359. pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
  360. pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
  361. pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
  362. pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
  363. pythinker_code/web/static/assets/blockDiagram-VD42YOAC-DzdKe497.js +122 -0
  364. pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
  365. pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
  366. pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
  367. pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-D84Blotg.js +10 -0
  368. pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
  369. pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
  370. pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  371. pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  372. pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  373. pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  374. pythinker_code/web/static/assets/channel-CllSjjdl.js +1 -0
  375. pythinker_code/web/static/assets/chunk-4BX2VUAB-C9w8wleE.js +1 -0
  376. pythinker_code/web/static/assets/chunk-55IACEB6-YlYJ8HnF.js +1 -0
  377. pythinker_code/web/static/assets/chunk-B4BG7PRW-Bwtz_AHU.js +165 -0
  378. pythinker_code/web/static/assets/chunk-DI55MBZ5-BbEHkl8h.js +220 -0
  379. pythinker_code/web/static/assets/chunk-FMBD7UC4-BKPbvjLC.js +15 -0
  380. pythinker_code/web/static/assets/chunk-QN33PNHL-D73dQvKf.js +1 -0
  381. pythinker_code/web/static/assets/chunk-QZHKN3VN-zGiLKes_.js +1 -0
  382. pythinker_code/web/static/assets/chunk-TZMSLE5B-LHJCi2fy.js +1 -0
  383. pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
  384. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-vX27iZwa.js +1 -0
  385. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-vX27iZwa.js +1 -0
  386. pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
  387. pythinker_code/web/static/assets/clone-DYBkaPm2.js +1 -0
  388. pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
  389. pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
  390. pythinker_code/web/static/assets/code-block-IT6T5CEO-NtKViZGl.js +2 -0
  391. pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
  392. pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
  393. pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
  394. pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
  395. pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
  396. pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-DialjZpd.js +1 -0
  397. pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
  398. pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
  399. pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
  400. pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
  401. pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
  402. pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
  403. pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
  404. pythinker_code/web/static/assets/cytoscape.esm-C_Fzpdck.js +321 -0
  405. pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
  406. pythinker_code/web/static/assets/dagre-6UL2VRFP-DfuvkZZ7.js +4 -0
  407. pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
  408. pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
  409. pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
  410. pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  411. pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
  412. pythinker_code/web/static/assets/diagram-PSM6KHXK-DLGctX3v.js +24 -0
  413. pythinker_code/web/static/assets/diagram-QEK2KX5R-DnxN6S0F.js +43 -0
  414. pythinker_code/web/static/assets/diagram-S2PKOQOG-Caq_Set9.js +24 -0
  415. pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
  416. pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
  417. pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
  418. pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
  419. pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  420. pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
  421. pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
  422. pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
  423. pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
  424. pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
  425. pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-BgTfALoK.js +60 -0
  426. pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
  427. pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
  428. pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  429. pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
  430. pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
  431. pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
  432. pythinker_code/web/static/assets/flowDiagram-NV44I4VS-QjW_fnGi.js +162 -0
  433. pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
  434. pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  435. pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
  436. pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
  437. pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-fqi8JFof.js +267 -0
  438. pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
  439. pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
  440. pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
  441. pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
  442. pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
  443. pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
  444. pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
  445. pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-i7o6VQ8x.js +65 -0
  446. pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
  447. pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  448. pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  449. pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  450. pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
  451. pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
  452. pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  453. pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
  454. pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
  455. pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
  456. pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
  457. pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
  458. pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
  459. pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
  460. pythinker_code/web/static/assets/graph-C0vZK2pT.js +1 -0
  461. pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
  462. pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
  463. pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  464. pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  465. pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  466. pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  467. pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  468. pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  469. pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
  470. pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
  471. pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
  472. pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
  473. pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
  474. pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
  475. pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
  476. pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
  477. pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
  478. pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
  479. pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
  480. pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
  481. pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
  482. pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
  483. pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
  484. pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
  485. pythinker_code/web/static/assets/index-BYCCk6-K.js +153 -0
  486. pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
  487. pythinker_code/web/static/assets/index-Cpy4G3uJ.js +2 -0
  488. pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
  489. pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
  490. pythinker_code/web/static/assets/index-DdIkp80K.js +5 -0
  491. pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-BMPpeZSM.js +2 -0
  492. pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
  493. pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
  494. pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  495. pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  496. pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  497. pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  498. pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  499. pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  500. pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  501. pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
  502. pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
  503. pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
  504. pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
  505. pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-DAM7gngo.js +139 -0
  506. pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
  507. pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
  508. pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
  509. pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
  510. pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
  511. pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
  512. pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
  513. pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
  514. pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  515. pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  516. pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  517. pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-ChpBpV0k.js +89 -0
  518. pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
  519. pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
  520. pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
  521. pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
  522. pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
  523. pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
  524. pythinker_code/web/static/assets/layout-C3Jp1gKO.js +1 -0
  525. pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
  526. pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
  527. pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
  528. pythinker_code/web/static/assets/linear-BGHtL1N4.js +1 -0
  529. pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
  530. pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
  531. pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
  532. pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
  533. pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
  534. pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
  535. pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
  536. pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
  537. pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
  538. pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
  539. pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  540. pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  541. pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  542. pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  543. pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
  544. pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
  545. pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
  546. pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
  547. pythinker_code/web/static/assets/mermaid-VLURNSYL-C_HW6koB.js +465 -0
  548. pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
  549. pythinker_code/web/static/assets/mermaid.core-CnT4VrPC.js +191 -0
  550. pythinker_code/web/static/assets/min-Dn5VRVmX.js +1 -0
  551. pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
  552. pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
  553. pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-x8EwhfIt.js +68 -0
  554. pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
  555. pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
  556. pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
  557. pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
  558. pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
  559. pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
  560. pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
  561. pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
  562. pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
  563. pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
  564. pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
  565. pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
  566. pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
  567. pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
  568. pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
  569. pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
  570. pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  571. pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
  572. pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
  573. pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
  574. pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
  575. pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
  576. pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
  577. pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-DgxBKGz2.js +30 -0
  578. pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
  579. pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
  580. pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
  581. pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
  582. pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
  583. pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
  584. pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
  585. pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
  586. pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
  587. pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
  588. pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
  589. pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
  590. pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
  591. pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
  592. pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
  593. pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
  594. pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
  595. pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
  596. pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
  597. pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DSpe_dqk.js +7 -0
  598. pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
  599. pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
  600. pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
  601. pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
  602. pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
  603. pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
  604. pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
  605. pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
  606. pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-8o9hozL-.js +64 -0
  607. pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
  608. pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  609. pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  610. pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
  611. pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
  612. pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
  613. pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
  614. pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
  615. pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-BLOSUixH.js +10 -0
  616. pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
  617. pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
  618. pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
  619. pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
  620. pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
  621. pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
  622. pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-6F2G8JTU.js +145 -0
  623. pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
  624. pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
  625. pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
  626. pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
  627. pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
  628. pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
  629. pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
  630. pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  631. pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
  632. pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
  633. pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
  634. pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
  635. pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
  636. pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
  637. pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
  638. pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
  639. pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DP8xP0iJ.js +1 -0
  640. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-1l6-EZNX.js +1 -0
  641. pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
  642. pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
  643. pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
  644. pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  645. pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
  646. pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
  647. pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
  648. pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
  649. pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
  650. pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
  651. pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
  652. pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
  653. pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-DMgruDfK.js +61 -0
  654. pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
  655. pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
  656. pythinker_code/web/static/assets/treemap-KMMF4GRG-Bo9ZkrAK.js +128 -0
  657. pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
  658. pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
  659. pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
  660. pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
  661. pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
  662. pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
  663. pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
  664. pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
  665. pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
  666. pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
  667. pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
  668. pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
  669. pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
  670. pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
  671. pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
  672. pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  673. pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  674. pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  675. pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
  676. pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
  677. pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
  678. pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
  679. pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
  680. pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
  681. pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
  682. pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
  683. pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
  684. pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
  685. pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
  686. pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
  687. pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
  688. pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-GeF2johi.js +7 -0
  689. pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
  690. pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
  691. pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
  692. pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
  693. pythinker_code/web/static/brand/arctecture.webp +0 -0
  694. pythinker_code/web/static/brand/bimi-logo.svg +46 -0
  695. pythinker_code/web/static/brand/favicon.ico +0 -0
  696. pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
  697. pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
  698. pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
  699. pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
  700. pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
  701. pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
  702. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
  703. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
  704. pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
  705. pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
  706. pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
  707. pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
  708. pythinker_code/web/static/brand/icon-192.png +0 -0
  709. pythinker_code/web/static/brand/icon-512.png +0 -0
  710. pythinker_code/web/static/brand/icon.svg +1 -0
  711. pythinker_code/web/static/brand/logo.png +0 -0
  712. pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
  713. pythinker_code/web/static/brand/robots.txt +4 -0
  714. pythinker_code/web/static/index.html +15 -0
  715. pythinker_code/web/static/logo.png +0 -0
  716. pythinker_code/web/store/__init__.py +1 -0
  717. pythinker_code/web/store/sessions.py +432 -0
  718. pythinker_code/wire/__init__.py +148 -0
  719. pythinker_code/wire/file.py +151 -0
  720. pythinker_code/wire/jsonrpc.py +263 -0
  721. pythinker_code/wire/protocol.py +2 -0
  722. pythinker_code/wire/root_hub.py +27 -0
  723. pythinker_code/wire/serde.py +26 -0
  724. pythinker_code/wire/server.py +1069 -0
  725. pythinker_code/wire/types.py +698 -0
  726. pythinker_code-2.0.0.dist-info/METADATA +660 -0
  727. pythinker_code-2.0.0.dist-info/RECORD +731 -0
  728. pythinker_code-2.0.0.dist-info/WHEEL +4 -0
  729. pythinker_code-2.0.0.dist-info/entry_points.txt +4 -0
  730. pythinker_code-2.0.0.dist-info/licenses/LICENSE +202 -0
  731. pythinker_code-2.0.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,1122 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import platform
7
+ import random
8
+ import socket
9
+ import sys
10
+ import tempfile
11
+ import time
12
+ import uuid
13
+ import webbrowser
14
+ from collections.abc import AsyncIterator
15
+ from contextlib import asynccontextmanager, suppress
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Any, Literal, cast
19
+
20
+ import aiohttp
21
+ import keyring
22
+ from pydantic import SecretStr
23
+
24
+ from pythinker_code.auth import PYTHINKER_CODE_PLATFORM_ID
25
+ from pythinker_code.auth.platforms import (
26
+ ModelInfo,
27
+ get_platform_by_id,
28
+ list_models,
29
+ managed_model_key,
30
+ managed_provider_key,
31
+ )
32
+ from pythinker_code.config import (
33
+ Config,
34
+ LLMModel,
35
+ LLMProvider,
36
+ OAuthRef,
37
+ PythinkerAIFetchConfig,
38
+ PythinkerAISearchConfig,
39
+ save_config,
40
+ )
41
+ from pythinker_code.constant import VERSION
42
+ from pythinker_code.share import get_share_dir
43
+ from pythinker_code.utils.aiohttp import new_client_session
44
+ from pythinker_code.utils.logging import logger
45
+
46
+ if TYPE_CHECKING:
47
+ from pythinker_code.soul.agent import Runtime
48
+
49
+
50
+ PYTHINKER_CODE_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"
51
+ PYTHINKER_CODE_OAUTH_KEY = "oauth/pythinker-code"
52
+ DEFAULT_OAUTH_HOST = "https://auth.pythinker.com"
53
+ KEYRING_SERVICE = "pythinker-code"
54
+ REFRESH_INTERVAL_SECONDS = 60
55
+ MIN_REFRESH_THRESHOLD_SECONDS = 300
56
+ REFRESH_THRESHOLD_RATIO = 0.5
57
+ UNAUTHORIZED_REFRESH_RETRY_COOLDOWN_SECONDS = 300
58
+ _CROSS_PROCESS_LOCK_RETRIES = 5
59
+ _RETRYABLE_REFRESH_STATUSES = {429, 500, 502, 503, 504}
60
+
61
+
62
+ def _refresh_threshold(expires_in: float) -> float:
63
+ """Return the dynamic refresh threshold in seconds."""
64
+ if expires_in > 0:
65
+ return max(MIN_REFRESH_THRESHOLD_SECONDS, expires_in * REFRESH_THRESHOLD_RATIO)
66
+ return MIN_REFRESH_THRESHOLD_SECONDS
67
+
68
+
69
+ class OAuthError(RuntimeError):
70
+ """OAuth flow error."""
71
+
72
+
73
+ class OAuthUnauthorized(OAuthError):
74
+ """OAuth credentials rejected."""
75
+
76
+
77
+ class _RetryableRefreshError(OAuthError):
78
+ """Transient HTTP error during token refresh (5xx / 429)."""
79
+
80
+
81
+ class OAuthDeviceExpired(OAuthError):
82
+ """Device authorization expired."""
83
+
84
+
85
+ OAuthEventKind = Literal["info", "error", "waiting", "verification_url", "success"]
86
+
87
+
88
+ @dataclass(slots=True, frozen=True)
89
+ class OAuthEvent:
90
+ type: OAuthEventKind
91
+ message: str
92
+ data: dict[str, Any] | None = None
93
+
94
+ def __str__(self) -> str:
95
+ return self.message
96
+
97
+ @property
98
+ def json(self) -> str:
99
+ payload: dict[str, Any] = {"type": self.type, "message": self.message}
100
+ if self.data is not None:
101
+ payload["data"] = self.data
102
+ return json.dumps(payload, ensure_ascii=False)
103
+
104
+
105
+ @dataclass(slots=True)
106
+ class OAuthToken:
107
+ access_token: str
108
+ refresh_token: str
109
+ expires_at: float
110
+ scope: str
111
+ token_type: str
112
+ expires_in: float = 0.0
113
+ account_id: str | None = None
114
+
115
+ @classmethod
116
+ def from_response(cls, payload: dict[str, Any]) -> OAuthToken:
117
+ expires_in = float(payload["expires_in"])
118
+ return cls(
119
+ access_token=str(payload["access_token"]),
120
+ refresh_token=str(payload["refresh_token"]),
121
+ expires_at=time.time() + expires_in,
122
+ scope=str(payload["scope"]),
123
+ token_type=str(payload["token_type"]),
124
+ expires_in=expires_in,
125
+ account_id=str(account_id) if (account_id := payload.get("account_id")) else None,
126
+ )
127
+
128
+ def to_dict(self) -> dict[str, Any]:
129
+ return {
130
+ "access_token": self.access_token,
131
+ "refresh_token": self.refresh_token,
132
+ "expires_at": self.expires_at,
133
+ "scope": self.scope,
134
+ "token_type": self.token_type,
135
+ "expires_in": self.expires_in,
136
+ "account_id": self.account_id,
137
+ }
138
+
139
+ @classmethod
140
+ def from_dict(cls, payload: dict[str, Any]) -> OAuthToken:
141
+ expires_at_value = payload.get("expires_at")
142
+ return cls(
143
+ access_token=str(payload.get("access_token") or ""),
144
+ refresh_token=str(payload.get("refresh_token") or ""),
145
+ expires_at=float(expires_at_value) if expires_at_value is not None else 0.0,
146
+ scope=str(payload.get("scope") or ""),
147
+ token_type=str(payload.get("token_type") or ""),
148
+ expires_in=float(payload.get("expires_in") or 0),
149
+ account_id=str(account_id) if (account_id := payload.get("account_id")) else None,
150
+ )
151
+
152
+
153
+ @dataclass(slots=True)
154
+ class _RejectedRefreshState:
155
+ refresh_token: str
156
+ retry_after: float
157
+
158
+
159
+ # Process-wide tombstone for refresh tokens that the server has rejected.
160
+ #
161
+ # Intentionally module-level rather than per-OAuthManager: OAuth credentials
162
+ # are a process-wide resource (all managers in this process share the same
163
+ # credentials file), so all managers should see the same "recently rejected"
164
+ # state. Without this, one manager's rejection would leave the persisted
165
+ # token visible to the next manager that loads from disk, and we'd re-issue
166
+ # the same dead refresh request and re-shadow a configured api_key fallback.
167
+ #
168
+ # Cross-process sharing is unnecessary — each process discovers the rejection
169
+ # independently on its first attempt, and the tombstone auto-clears when the
170
+ # on-disk refresh_token differs from the rejected one (i.e. another process
171
+ # successfully rotated, or /login atomically rewrote the file).
172
+ _REJECTED_REFRESH_TOKENS: dict[str, _RejectedRefreshState] = {}
173
+
174
+
175
+ @dataclass(slots=True)
176
+ class DeviceAuthorization:
177
+ user_code: str
178
+ device_code: str
179
+ verification_uri: str
180
+ verification_uri_complete: str
181
+ expires_in: int | None
182
+ interval: int
183
+
184
+
185
+ def _oauth_host() -> str:
186
+ return (
187
+ os.getenv("PYTHINKER_CODE_OAUTH_HOST")
188
+ or os.getenv("PYTHINKER_OAUTH_HOST")
189
+ or DEFAULT_OAUTH_HOST
190
+ )
191
+
192
+
193
+ def _device_id_path() -> Path:
194
+ return get_share_dir() / "device_id"
195
+
196
+
197
+ def _ensure_private_file(path: Path) -> None:
198
+ with suppress(OSError):
199
+ os.chmod(path, 0o600)
200
+
201
+
202
+ def _device_model() -> str:
203
+ system = platform.system()
204
+ arch = platform.machine() or ""
205
+ if system == "Darwin":
206
+ version = platform.mac_ver()[0] or platform.release()
207
+ if version and arch:
208
+ return f"macOS {version} {arch}"
209
+ if version:
210
+ return f"macOS {version}"
211
+ return f"macOS {arch}".strip()
212
+ if system == "Windows":
213
+ release = platform.release()
214
+ if release == "10":
215
+ try:
216
+ build = sys.getwindowsversion().build # type: ignore[attr-defined]
217
+ except Exception:
218
+ build = None
219
+ if build and build >= 22000:
220
+ release = "11"
221
+ if release and arch:
222
+ return f"Windows {release} {arch}"
223
+ if release:
224
+ return f"Windows {release}"
225
+ return f"Windows {arch}".strip()
226
+ if system:
227
+ version = platform.release()
228
+ if version and arch:
229
+ return f"{system} {version} {arch}"
230
+ if version:
231
+ return f"{system} {version}"
232
+ return f"{system} {arch}".strip()
233
+ return "Unknown"
234
+
235
+
236
+ def get_device_id() -> str:
237
+ path = _device_id_path()
238
+ if path.exists():
239
+ return path.read_text(encoding="utf-8").strip()
240
+ device_id = uuid.uuid4().hex
241
+ path.write_text(device_id, encoding="utf-8")
242
+ _ensure_private_file(path)
243
+ from pythinker_code.telemetry import track
244
+
245
+ track("first_launch")
246
+ return device_id
247
+
248
+
249
+ def _ascii_header_value(value: str, *, fallback: str = "unknown") -> str:
250
+ sanitized = "".join(ch for ch in value if ord(ch) < 128).strip()
251
+ return sanitized or fallback
252
+
253
+
254
+ def _common_headers() -> dict[str, str]:
255
+ device_name = platform.node() or socket.gethostname()
256
+ device_model = _device_model()
257
+ headers = {
258
+ "X-Msh-Platform": "pythinker_code",
259
+ "X-Msh-Version": VERSION,
260
+ "X-Msh-Device-Name": device_name,
261
+ "X-Msh-Device-Model": device_model,
262
+ "X-Msh-Os-Version": platform.version(),
263
+ "X-Msh-Device-Id": get_device_id(),
264
+ }
265
+ return {key: _ascii_header_value(value) for key, value in headers.items()}
266
+
267
+
268
+ def _credentials_dir() -> Path:
269
+ path = get_share_dir() / "credentials"
270
+ path.mkdir(parents=True, exist_ok=True)
271
+ return path
272
+
273
+
274
+ def _credentials_path(key: str) -> Path:
275
+ name = key.removeprefix("oauth/").split("/")[-1] or key
276
+ return _credentials_dir() / f"{name}.json"
277
+
278
+
279
+ def _credentials_lock_path(key: str) -> Path:
280
+ name = key.removeprefix("oauth/").split("/")[-1] or key
281
+ return _credentials_dir() / f"{name}.lock"
282
+
283
+
284
+ class _CrossProcessLock:
285
+ """File-based lock that coordinates token refresh across pythinker-code processes.
286
+
287
+ Uses fcntl.flock on Unix and msvcrt.locking on Windows.
288
+ """
289
+
290
+ def __init__(self, key: str) -> None:
291
+ self._path = _credentials_lock_path(key)
292
+ self._fd: int | None = None
293
+
294
+ def _acquire(self) -> bool:
295
+ """Acquire the lock.
296
+
297
+ Returns ``True`` if locked, ``False`` on contention.
298
+ Raises ``OSError`` if the lock file cannot be opened (permanent failure).
299
+ """
300
+ self._fd = os.open(str(self._path), os.O_CREAT | os.O_RDWR, 0o600)
301
+ try:
302
+ if sys.platform == "win32":
303
+ import msvcrt
304
+
305
+ # msvcrt.locking requires bytes to exist at the lock position.
306
+ if os.fstat(self._fd).st_size == 0:
307
+ os.write(self._fd, b"\0")
308
+ os.lseek(self._fd, 0, os.SEEK_SET)
309
+ msvcrt.locking(self._fd, msvcrt.LK_NBLCK, 1)
310
+ else:
311
+ import fcntl
312
+
313
+ fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
314
+ return True
315
+ except OSError:
316
+ os.close(self._fd)
317
+ self._fd = None
318
+ return False
319
+
320
+ def release(self) -> None:
321
+ if self._fd is not None:
322
+ try:
323
+ if sys.platform == "win32":
324
+ import msvcrt
325
+
326
+ with suppress(OSError):
327
+ os.lseek(self._fd, 0, os.SEEK_SET)
328
+ msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1)
329
+ finally:
330
+ with suppress(OSError):
331
+ os.close(self._fd)
332
+ self._fd = None
333
+
334
+ async def acquire_with_retry(self) -> bool:
335
+ for _attempt in range(_CROSS_PROCESS_LOCK_RETRIES):
336
+ try:
337
+ if self._acquire():
338
+ return True
339
+ except OSError:
340
+ # Cannot open/create the lock file (permissions, read-only FS, etc.).
341
+ # Permanent failure — skip backoff and fall back to unlocked refresh.
342
+ return False
343
+ await asyncio.sleep(1 + random.random())
344
+ # After waiting, re-check if the token was refreshed by the holder.
345
+ try:
346
+ return self._acquire()
347
+ except OSError:
348
+ return False
349
+
350
+ async def __aenter__(self) -> bool:
351
+ return await self.acquire_with_retry()
352
+
353
+ async def __aexit__(self, *args: object) -> None:
354
+ self.release()
355
+
356
+
357
+ def _load_from_keyring(key: str) -> OAuthToken | None:
358
+ try:
359
+ raw = keyring.get_password(KEYRING_SERVICE, key)
360
+ except Exception as exc:
361
+ logger.warning("Failed to read token from keyring: {error}", error=exc)
362
+ return None
363
+ if not raw:
364
+ return None
365
+ try:
366
+ payload = json.loads(raw)
367
+ except json.JSONDecodeError:
368
+ return None
369
+ if not isinstance(payload, dict):
370
+ return None
371
+ payload = cast(dict[str, Any], payload)
372
+ return OAuthToken.from_dict(payload)
373
+
374
+
375
+ def _delete_from_keyring(key: str) -> None:
376
+ try:
377
+ keyring.delete_password(KEYRING_SERVICE, key)
378
+ except Exception:
379
+ return
380
+
381
+
382
+ def _load_from_file(key: str) -> OAuthToken | None:
383
+ path = _credentials_path(key)
384
+ if not path.exists():
385
+ return None
386
+ try:
387
+ payload = json.loads(path.read_text(encoding="utf-8"))
388
+ except (json.JSONDecodeError, OSError):
389
+ return None
390
+ if not isinstance(payload, dict):
391
+ return None
392
+ payload = cast(dict[str, Any], payload)
393
+ return OAuthToken.from_dict(payload)
394
+
395
+
396
+ def _save_to_file(key: str, token: OAuthToken) -> None:
397
+ path = _credentials_path(key)
398
+ fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
399
+ try:
400
+ data = json.dumps(token.to_dict(), ensure_ascii=False).encode("utf-8")
401
+ written = os.write(fd, data)
402
+ if written != len(data):
403
+ raise OSError(f"Short write: {written}/{len(data)} bytes")
404
+ os.fsync(fd)
405
+ os.close(fd)
406
+ fd = -1
407
+ with suppress(OSError):
408
+ os.chmod(tmp_path, 0o600)
409
+ os.replace(tmp_path, path)
410
+ except BaseException:
411
+ if fd >= 0:
412
+ with suppress(OSError):
413
+ os.close(fd)
414
+ with suppress(OSError):
415
+ os.unlink(tmp_path)
416
+ raise
417
+
418
+
419
+ def _delete_from_file(key: str) -> None:
420
+ path = _credentials_path(key)
421
+ if path.exists():
422
+ path.unlink()
423
+
424
+
425
+ def load_tokens(ref: OAuthRef) -> OAuthToken | None:
426
+ file_token = _load_from_file(ref.key)
427
+ if file_token is not None:
428
+ return file_token
429
+ if ref.storage != "keyring":
430
+ return None
431
+ token = _load_from_keyring(ref.key)
432
+ if token is None:
433
+ return None
434
+ try:
435
+ _save_to_file(ref.key, token)
436
+ except OSError as exc:
437
+ logger.warning("Failed to migrate token from keyring to file: {error}", error=exc)
438
+ else:
439
+ with suppress(Exception):
440
+ _delete_from_keyring(ref.key)
441
+ return token
442
+
443
+
444
+ def save_tokens(ref: OAuthRef, token: OAuthToken) -> OAuthRef:
445
+ if ref.storage == "keyring":
446
+ logger.warning("Keyring storage is deprecated; saving OAuth tokens to file.")
447
+ ref = OAuthRef(storage="file", key=ref.key)
448
+ _save_to_file(ref.key, token)
449
+ return ref
450
+
451
+
452
+ def delete_tokens(ref: OAuthRef) -> None:
453
+ if ref.storage == "keyring":
454
+ _delete_from_keyring(ref.key)
455
+ _delete_from_file(ref.key)
456
+
457
+
458
+ async def request_device_authorization() -> DeviceAuthorization:
459
+ async with (
460
+ new_client_session() as session,
461
+ session.post(
462
+ f"{_oauth_host().rstrip('/')}/api/oauth/device_authorization",
463
+ data={"client_id": PYTHINKER_CODE_CLIENT_ID},
464
+ headers=_common_headers(),
465
+ ) as response,
466
+ ):
467
+ data = await response.json(content_type=None)
468
+ status = response.status
469
+ if status != 200:
470
+ raise OAuthError(f"Device authorization failed: {data}")
471
+ return DeviceAuthorization(
472
+ user_code=str(data["user_code"]),
473
+ device_code=str(data["device_code"]),
474
+ verification_uri=str(data.get("verification_uri") or ""),
475
+ verification_uri_complete=str(data["verification_uri_complete"]),
476
+ expires_in=int(data.get("expires_in") or 0) or None,
477
+ interval=int(data.get("interval") or 5),
478
+ )
479
+
480
+
481
+ async def _request_device_token(auth: DeviceAuthorization) -> tuple[int, dict[str, Any]]:
482
+ try:
483
+ async with (
484
+ new_client_session() as session,
485
+ session.post(
486
+ f"{_oauth_host().rstrip('/')}/api/oauth/token",
487
+ data={
488
+ "client_id": PYTHINKER_CODE_CLIENT_ID,
489
+ "device_code": auth.device_code,
490
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
491
+ },
492
+ headers=_common_headers(),
493
+ ) as response,
494
+ ):
495
+ data_any: Any = await response.json(content_type=None)
496
+ status = response.status
497
+ except aiohttp.ClientError as exc:
498
+ raise OAuthError("Token polling request failed.") from exc
499
+ if not isinstance(data_any, dict):
500
+ raise OAuthError("Unexpected token polling response.")
501
+ data = cast(dict[str, Any], data_any)
502
+ if status >= 500:
503
+ raise OAuthError(f"Token polling server error: {status}.")
504
+ return status, data
505
+
506
+
507
+ async def refresh_token(refresh_token: str, *, max_retries: int = 3) -> OAuthToken:
508
+ last_exc: Exception | None = None
509
+ for attempt in range(max_retries):
510
+ try:
511
+ async with (
512
+ new_client_session() as session,
513
+ session.post(
514
+ f"{_oauth_host().rstrip('/')}/api/oauth/token",
515
+ data={
516
+ "client_id": PYTHINKER_CODE_CLIENT_ID,
517
+ "grant_type": "refresh_token",
518
+ "refresh_token": refresh_token,
519
+ },
520
+ headers=_common_headers(),
521
+ ) as response,
522
+ ):
523
+ status = response.status
524
+ data: dict[str, Any]
525
+ try:
526
+ data = await response.json(content_type=None)
527
+ except (json.JSONDecodeError, aiohttp.ContentTypeError):
528
+ data = {}
529
+ if status in (401, 403):
530
+ raise OAuthUnauthorized(
531
+ data.get("error_description") or "Token refresh unauthorized."
532
+ )
533
+ if status != 200:
534
+ desc = data.get("error_description") or f"Token refresh failed (HTTP {status})."
535
+ if status in _RETRYABLE_REFRESH_STATUSES:
536
+ raise _RetryableRefreshError(desc)
537
+ raise OAuthError(desc)
538
+ return OAuthToken.from_response(data)
539
+ except OAuthUnauthorized:
540
+ raise
541
+ except (aiohttp.ClientError, TimeoutError, OSError, _RetryableRefreshError) as exc:
542
+ last_exc = exc
543
+ if attempt < max_retries - 1:
544
+ await asyncio.sleep(2**attempt)
545
+ logger.warning(
546
+ "Token refresh attempt {attempt} failed, retrying: {error}",
547
+ attempt=attempt + 1,
548
+ error=exc,
549
+ )
550
+ raise OAuthError("Token refresh failed after retries.") from last_exc
551
+
552
+
553
+ def _select_default_model_and_thinking(models: list[ModelInfo]) -> tuple[ModelInfo, bool] | None:
554
+ if not models:
555
+ return None
556
+ selected_model = models[0]
557
+ capabilities = selected_model.capabilities
558
+ thinking = "thinking" in capabilities or "always_thinking" in capabilities
559
+ return selected_model, thinking
560
+
561
+
562
+ def _apply_pythinker_code_config(
563
+ config: Config,
564
+ *,
565
+ models: list[ModelInfo],
566
+ selected_model: ModelInfo,
567
+ thinking: bool,
568
+ oauth_ref: OAuthRef,
569
+ ) -> None:
570
+ platform = get_platform_by_id(PYTHINKER_CODE_PLATFORM_ID)
571
+ if platform is None:
572
+ raise OAuthError("Pythinker platform not found.")
573
+
574
+ provider_key = managed_provider_key(platform.id)
575
+ config.providers[provider_key] = LLMProvider(
576
+ type="pythinker",
577
+ base_url=platform.base_url,
578
+ api_key=SecretStr(""),
579
+ oauth=oauth_ref,
580
+ )
581
+
582
+ for key, model in list(config.models.items()):
583
+ if model.provider == provider_key:
584
+ del config.models[key]
585
+
586
+ for model_info in models:
587
+ capabilities = model_info.capabilities or None
588
+ config.models[managed_model_key(platform.id, model_info.id)] = LLMModel(
589
+ provider=provider_key,
590
+ model=model_info.id,
591
+ max_context_size=model_info.context_length,
592
+ capabilities=capabilities,
593
+ display_name=model_info.display_name,
594
+ )
595
+
596
+ config.default_model = managed_model_key(platform.id, selected_model.id)
597
+ config.default_thinking = thinking
598
+
599
+ if platform.search_url:
600
+ config.services.pythinker_ai_search = PythinkerAISearchConfig(
601
+ base_url=platform.search_url,
602
+ api_key=SecretStr(""),
603
+ oauth=oauth_ref,
604
+ )
605
+
606
+ if platform.fetch_url:
607
+ config.services.pythinker_ai_fetch = PythinkerAIFetchConfig(
608
+ base_url=platform.fetch_url,
609
+ api_key=SecretStr(""),
610
+ oauth=oauth_ref,
611
+ )
612
+
613
+
614
+ async def login_pythinker_code(
615
+ config: Config, *, open_browser: bool = True
616
+ ) -> AsyncIterator[OAuthEvent]:
617
+ if not config.is_from_default_location:
618
+ yield OAuthEvent(
619
+ "error",
620
+ "Login requires the default config file; restart without --config/--config-file.",
621
+ )
622
+ return
623
+
624
+ platform = get_platform_by_id(PYTHINKER_CODE_PLATFORM_ID)
625
+ if platform is None:
626
+ yield OAuthEvent("error", "Pythinker platform is unavailable.")
627
+ return
628
+
629
+ auth: DeviceAuthorization
630
+ token: OAuthToken | None = None
631
+ while True:
632
+ try:
633
+ auth = await request_device_authorization()
634
+ except Exception as exc:
635
+ yield OAuthEvent("error", f"Login failed: {exc}")
636
+ return
637
+
638
+ yield OAuthEvent(
639
+ "info",
640
+ "Please visit the following URL to finish authorization.",
641
+ )
642
+ yield OAuthEvent(
643
+ "verification_url",
644
+ f"Verification URL: {auth.verification_uri_complete}",
645
+ data={
646
+ "verification_url": auth.verification_uri_complete,
647
+ "user_code": auth.user_code,
648
+ },
649
+ )
650
+ if open_browser:
651
+ try:
652
+ webbrowser.open(auth.verification_uri_complete)
653
+ except Exception as exc:
654
+ logger.warning("Failed to open browser: {error}", error=exc)
655
+
656
+ interval = max(auth.interval, 1)
657
+ printed_wait = False
658
+ try:
659
+ while True:
660
+ status, data = await _request_device_token(auth)
661
+ if status == 200 and "access_token" in data:
662
+ token = OAuthToken.from_response(data)
663
+ break
664
+ error_code = str(data.get("error") or "unknown_error")
665
+ if error_code == "expired_token":
666
+ raise OAuthDeviceExpired("Device code expired.")
667
+ error_description = str(data.get("error_description") or "")
668
+ if not printed_wait:
669
+ yield OAuthEvent(
670
+ "waiting",
671
+ f"Waiting for user authorization...: {error_description.strip()}",
672
+ data={
673
+ "error": error_code,
674
+ "error_description": error_description,
675
+ },
676
+ )
677
+ printed_wait = True
678
+ await asyncio.sleep(interval)
679
+ except OAuthDeviceExpired:
680
+ yield OAuthEvent("info", "Device code expired, restarting login...")
681
+ continue
682
+ except Exception as exc:
683
+ yield OAuthEvent("error", f"Login failed: {exc}")
684
+ return
685
+ break
686
+
687
+ assert token is not None
688
+
689
+ oauth_ref = OAuthRef(storage="file", key=PYTHINKER_CODE_OAUTH_KEY)
690
+ oauth_ref = save_tokens(oauth_ref, token)
691
+
692
+ try:
693
+ models = await list_models(platform, token.access_token)
694
+ except Exception as exc:
695
+ logger.error("Failed to get models: {error}", error=exc)
696
+ yield OAuthEvent("error", f"Failed to get models: {exc}")
697
+ return
698
+
699
+ if not models:
700
+ yield OAuthEvent("error", "No models available for the selected platform.")
701
+ return
702
+
703
+ selection = _select_default_model_and_thinking(models)
704
+ if selection is None:
705
+ return
706
+ selected_model, thinking = selection
707
+
708
+ _apply_pythinker_code_config(
709
+ config,
710
+ models=models,
711
+ selected_model=selected_model,
712
+ thinking=thinking,
713
+ oauth_ref=oauth_ref,
714
+ )
715
+ save_config(config)
716
+ yield OAuthEvent("success", "Logged in successfully.")
717
+ return
718
+
719
+
720
+ async def logout_pythinker_code(config: Config) -> AsyncIterator[OAuthEvent]:
721
+ if not config.is_from_default_location:
722
+ yield OAuthEvent(
723
+ "error",
724
+ "Logout requires the default config file; restart without --config/--config-file.",
725
+ )
726
+ return
727
+
728
+ delete_tokens(OAuthRef(storage="keyring", key=PYTHINKER_CODE_OAUTH_KEY))
729
+ delete_tokens(OAuthRef(storage="file", key=PYTHINKER_CODE_OAUTH_KEY))
730
+
731
+ provider_key = managed_provider_key(PYTHINKER_CODE_PLATFORM_ID)
732
+ if provider_key in config.providers:
733
+ del config.providers[provider_key]
734
+
735
+ removed_default = False
736
+ for key, model in list(config.models.items()):
737
+ if model.provider != provider_key:
738
+ continue
739
+ del config.models[key]
740
+ if config.default_model == key:
741
+ removed_default = True
742
+
743
+ if removed_default:
744
+ config.default_model = ""
745
+
746
+ config.services.pythinker_ai_search = None
747
+ config.services.pythinker_ai_fetch = None
748
+
749
+ save_config(config)
750
+ yield OAuthEvent("success", "Logged out successfully.")
751
+ return
752
+
753
+
754
+ class OAuthManager:
755
+ def __init__(self, config: Config) -> None:
756
+ self._config = config
757
+ # Cache access tokens only; refresh tokens are always read from persisted storage.
758
+ self._access_tokens: dict[str, str] = {}
759
+ self._refresh_lock = asyncio.Lock()
760
+ self._migrate_oauth_storage()
761
+ self._load_initial_tokens()
762
+
763
+ def _iter_oauth_refs(self) -> list[OAuthRef]:
764
+ refs: list[OAuthRef] = []
765
+ for provider in self._config.providers.values():
766
+ if provider.oauth:
767
+ refs.append(provider.oauth)
768
+ for service in (
769
+ self._config.services.pythinker_ai_search,
770
+ self._config.services.pythinker_ai_fetch,
771
+ ):
772
+ if service and service.oauth:
773
+ refs.append(service.oauth)
774
+ return refs
775
+
776
+ def _migrate_oauth_storage(self) -> None:
777
+ migrated_keys: set[str] = set()
778
+ changed = False
779
+
780
+ def _migrate_ref(ref: OAuthRef) -> OAuthRef:
781
+ nonlocal changed
782
+ if ref.storage != "keyring":
783
+ return ref
784
+ if ref.key not in migrated_keys:
785
+ load_tokens(ref)
786
+ migrated_keys.add(ref.key)
787
+ changed = True
788
+ return OAuthRef(storage="file", key=ref.key)
789
+
790
+ for provider in self._config.providers.values():
791
+ if provider.oauth:
792
+ provider.oauth = _migrate_ref(provider.oauth)
793
+
794
+ for service in (
795
+ self._config.services.pythinker_ai_search,
796
+ self._config.services.pythinker_ai_fetch,
797
+ ):
798
+ if service and service.oauth:
799
+ service.oauth = _migrate_ref(service.oauth)
800
+
801
+ if changed and self._config.is_from_default_location:
802
+ save_config(self._config)
803
+
804
+ def _load_initial_tokens(self) -> None:
805
+ for ref in self._iter_oauth_refs():
806
+ token = load_tokens(ref)
807
+ if token and not self._should_suppress_persisted_token(ref, token):
808
+ self._cache_access_token(ref, token)
809
+
810
+ def _rejected_refresh_state(
811
+ self, ref: OAuthRef, refresh_token: str | None
812
+ ) -> _RejectedRefreshState | None:
813
+ if not refresh_token:
814
+ return None
815
+ state = _REJECTED_REFRESH_TOKENS.get(ref.key)
816
+ if state and state.refresh_token != refresh_token:
817
+ _REJECTED_REFRESH_TOKENS.pop(ref.key, None)
818
+ return None
819
+ return state
820
+
821
+ def _should_suppress_persisted_token(self, ref: OAuthRef, token: OAuthToken) -> bool:
822
+ return self._rejected_refresh_state(ref, token.refresh_token) is not None
823
+
824
+ def _can_retry_rejected_refresh_token(self, ref: OAuthRef, refresh_token: str | None) -> bool:
825
+ state = self._rejected_refresh_state(ref, refresh_token)
826
+ return state is None or time.time() >= state.retry_after
827
+
828
+ def _mark_refresh_token_rejected(self, ref: OAuthRef, refresh_token: str) -> None:
829
+ if not refresh_token:
830
+ return
831
+ _REJECTED_REFRESH_TOKENS[ref.key] = _RejectedRefreshState(
832
+ refresh_token=refresh_token,
833
+ retry_after=time.time() + UNAUTHORIZED_REFRESH_RETRY_COOLDOWN_SECONDS,
834
+ )
835
+
836
+ def _clear_rejected_refresh_token(self, ref: OAuthRef) -> None:
837
+ _REJECTED_REFRESH_TOKENS.pop(ref.key, None)
838
+
839
+ def _cache_access_token(self, ref: OAuthRef, token: OAuthToken) -> None:
840
+ if not token.access_token:
841
+ self._access_tokens.pop(ref.key, None)
842
+ return
843
+ self._access_tokens[ref.key] = token.access_token
844
+
845
+ def get_cached_access_token(self, key: str) -> str | None:
846
+ """Get a cached access token by key, or None if not available."""
847
+ return self._access_tokens.get(key)
848
+
849
+ def common_headers(self) -> dict[str, str]:
850
+ return _common_headers()
851
+
852
+ def resolve_api_key(self, api_key: SecretStr, oauth: OAuthRef | None) -> str:
853
+ if oauth:
854
+ token = self._access_tokens.get(oauth.key)
855
+ if token is None:
856
+ persisted = load_tokens(oauth)
857
+ if persisted and not self._should_suppress_persisted_token(oauth, persisted):
858
+ self._cache_access_token(oauth, persisted)
859
+ token = self._access_tokens.get(oauth.key)
860
+ if token:
861
+ return token
862
+ logger.warning(
863
+ "OAuth ref present (key={key}) but no access token resolved; "
864
+ "falling back to configured api_key",
865
+ key=oauth.key,
866
+ )
867
+ return api_key.get_secret_value()
868
+
869
+ def get_chatgpt_account_id(self, oauth: OAuthRef | None) -> str | None:
870
+ """Return the ChatGPT account_id persisted alongside an OAuth token.
871
+
872
+ Used by the `/usage` adapter for ChatGPT Codex which requires the
873
+ `ChatGPT-Account-Id` header in addition to the bearer token.
874
+ """
875
+ if oauth is None:
876
+ return None
877
+ persisted = load_tokens(oauth)
878
+ if persisted is None:
879
+ return None
880
+ account_id = getattr(persisted, "account_id", None)
881
+ return str(account_id) if account_id else None
882
+
883
+ def _pythinker_code_ref(self) -> OAuthRef | None:
884
+ provider_key = managed_provider_key(PYTHINKER_CODE_PLATFORM_ID)
885
+ provider = self._config.providers.get(provider_key)
886
+ if provider and provider.oauth:
887
+ return provider.oauth
888
+ for service in (
889
+ self._config.services.pythinker_ai_search,
890
+ self._config.services.pythinker_ai_fetch,
891
+ ):
892
+ if service and service.oauth and service.oauth.key == PYTHINKER_CODE_OAUTH_KEY:
893
+ return service.oauth
894
+ return None
895
+
896
+ async def ensure_fresh(self, runtime: Runtime | None = None, *, force: bool = False) -> None:
897
+ """Load persisted tokens, cache them, and refresh if close to expiry.
898
+
899
+ Args:
900
+ runtime: When provided the live LLM client's API key is updated
901
+ in-place. Pass ``None`` for lightweight callers (e.g. title
902
+ generation) that only need the internal cache to be current.
903
+ force: When True, skip the expiry-threshold check and always
904
+ attempt a refresh. Used after receiving a 401 from the server.
905
+ """
906
+ for ref in self._iter_oauth_refs():
907
+ token = load_tokens(ref)
908
+ if token is None:
909
+ continue
910
+ if self._should_suppress_persisted_token(ref, token):
911
+ self._access_tokens.pop(ref.key, None)
912
+ self._apply_access_token(runtime, ref, "")
913
+ if not self._can_retry_rejected_refresh_token(ref, token.refresh_token):
914
+ if force:
915
+ raise OAuthUnauthorized("Refresh token was recently rejected.")
916
+ continue
917
+ else:
918
+ self._cache_access_token(ref, token)
919
+ if token.access_token:
920
+ self._apply_access_token(runtime, ref, token.access_token)
921
+ await self._refresh_tokens(ref, token, runtime, force=force)
922
+
923
+ @asynccontextmanager
924
+ async def refreshing(self, runtime: Runtime) -> AsyncIterator[None]:
925
+ stop_event = asyncio.Event()
926
+
927
+ async def _runner() -> None:
928
+ try:
929
+ while True:
930
+ wall_before = time.time()
931
+ try:
932
+ await asyncio.wait_for(
933
+ stop_event.wait(),
934
+ timeout=REFRESH_INTERVAL_SECONDS,
935
+ )
936
+ return
937
+ except TimeoutError:
938
+ pass
939
+ elapsed = time.time() - wall_before
940
+ force = elapsed > REFRESH_INTERVAL_SECONDS * 2
941
+ if force:
942
+ logger.info(
943
+ "Detected possible sleep/wake ({elapsed:.0f}s elapsed), "
944
+ "forcing token refresh.",
945
+ elapsed=elapsed,
946
+ )
947
+ try:
948
+ await self.ensure_fresh(runtime, force=force)
949
+ except Exception as exc:
950
+ logger.warning(
951
+ "Failed to refresh OAuth token in background: {error}",
952
+ error=exc,
953
+ )
954
+ except asyncio.CancelledError:
955
+ pass
956
+
957
+ await self.ensure_fresh(runtime)
958
+ refresh_task = asyncio.create_task(_runner())
959
+ try:
960
+ yield
961
+ finally:
962
+ stop_event.set()
963
+ refresh_task.cancel()
964
+ with suppress(asyncio.CancelledError):
965
+ await refresh_task
966
+
967
+ async def _refresh_tokens(
968
+ self,
969
+ ref: OAuthRef,
970
+ token: OAuthToken,
971
+ runtime: Runtime | None,
972
+ *,
973
+ force: bool = False,
974
+ ) -> None:
975
+ # Always prefer persisted tokens before refresh to avoid stale cache
976
+ # when multiple sessions might have already rotated the refresh token.
977
+ persisted = load_tokens(ref)
978
+ if persisted and not self._should_suppress_persisted_token(ref, persisted):
979
+ self._cache_access_token(ref, persisted)
980
+ current_token = persisted or token
981
+ if not current_token.refresh_token:
982
+ return
983
+ async with self._refresh_lock:
984
+ # Re-check persisted token inside the in-process lock.
985
+ persisted = load_tokens(ref)
986
+ if persisted and not self._should_suppress_persisted_token(ref, persisted):
987
+ self._cache_access_token(ref, persisted)
988
+ current = persisted or current_token
989
+ if not force:
990
+ now = time.time()
991
+ if (
992
+ current.expires_at
993
+ and current.expires_at > now
994
+ and current.expires_at - now >= _refresh_threshold(current.expires_in)
995
+ ):
996
+ return
997
+ refresh_token_value = current.refresh_token
998
+ if not refresh_token_value:
999
+ return
1000
+ if self._should_suppress_persisted_token(
1001
+ ref, current
1002
+ ) and not self._can_retry_rejected_refresh_token(ref, refresh_token_value):
1003
+ self._access_tokens.pop(ref.key, None)
1004
+ self._apply_access_token(runtime, ref, "")
1005
+ if force:
1006
+ raise OAuthUnauthorized("Refresh token was recently rejected.")
1007
+ return
1008
+
1009
+ # Acquire cross-process file lock to coordinate with other
1010
+ # pythinker-code instances (terminal, VS Code, web).
1011
+ xlock = _CrossProcessLock(ref.key)
1012
+ acquired = await xlock.acquire_with_retry()
1013
+ try:
1014
+ if acquired:
1015
+ # Triple-check after acquiring the lock — another process
1016
+ # may have refreshed while we waited.
1017
+ locked_token = load_tokens(ref)
1018
+ if locked_token and locked_token.refresh_token != refresh_token_value:
1019
+ self._clear_rejected_refresh_token(ref)
1020
+ self._cache_access_token(ref, locked_token)
1021
+ self._apply_access_token(runtime, ref, locked_token.access_token)
1022
+ return
1023
+ if not force and locked_token:
1024
+ remaining = locked_token.expires_at - time.time()
1025
+ if locked_token.expires_at and remaining >= _refresh_threshold(
1026
+ locked_token.expires_in
1027
+ ):
1028
+ self._clear_rejected_refresh_token(ref)
1029
+ self._cache_access_token(ref, locked_token)
1030
+ self._apply_access_token(runtime, ref, locked_token.access_token)
1031
+ return
1032
+ else:
1033
+ logger.warning("Could not acquire cross-process lock for token refresh")
1034
+
1035
+ try:
1036
+ refreshed = await self._refresh_token_for_ref(ref, refresh_token_value)
1037
+ except OAuthUnauthorized as exc:
1038
+ # Give a concurrent instance time to persist its rotated token.
1039
+ await asyncio.sleep(1)
1040
+ latest = load_tokens(ref)
1041
+ if latest and latest.refresh_token != refresh_token_value:
1042
+ self._clear_rejected_refresh_token(ref)
1043
+ self._cache_access_token(ref, latest)
1044
+ self._apply_access_token(runtime, ref, latest.access_token)
1045
+ return
1046
+ # delete_tokens(ref) would remove whatever the ref points
1047
+ # to on disk right now, not "the refresh_token that just
1048
+ # got 401". A concurrent OAuthManager (another process,
1049
+ # or another manager in this process — app.py:199,
1050
+ # web/api/sessions.py:817, plugin paths) may have
1051
+ # legitimately rotated and written a valid new token into
1052
+ # this file between the load_tokens check above and here,
1053
+ # and we'd wipe it. Clearing the in-memory cache is
1054
+ # enough; a short in-process tombstone prevents the same
1055
+ # rejected refresh_token from being immediately retried or
1056
+ # preferred over a configured static api_key fallback, and
1057
+ # /login still atomically overwrites the file.
1058
+ self._mark_refresh_token_rejected(ref, refresh_token_value)
1059
+ self._access_tokens.pop(ref.key, None)
1060
+ self._apply_access_token(runtime, ref, "")
1061
+ if force:
1062
+ raise
1063
+ logger.warning(
1064
+ "OAuth credentials rejected: {error}",
1065
+ error=exc,
1066
+ )
1067
+ from pythinker_code.telemetry import track
1068
+
1069
+ track("oauth_refresh", success=False, reason="unauthorized")
1070
+ return
1071
+ except Exception as exc:
1072
+ if force:
1073
+ raise
1074
+ logger.warning("Failed to refresh OAuth token: {error}", error=exc)
1075
+ from pythinker_code.telemetry import track
1076
+
1077
+ track("oauth_refresh", success=False, reason="network_or_other")
1078
+ return
1079
+ if refreshed.account_id is None:
1080
+ refreshed.account_id = current.account_id
1081
+ self._clear_rejected_refresh_token(ref)
1082
+ save_tokens(ref, refreshed)
1083
+ self._cache_access_token(ref, refreshed)
1084
+ self._apply_access_token(runtime, ref, refreshed.access_token)
1085
+ from pythinker_code.telemetry import track
1086
+
1087
+ track("oauth_refresh", success=True)
1088
+ finally:
1089
+ xlock.release()
1090
+
1091
+ async def _refresh_token_for_ref(self, ref: OAuthRef, refresh_token_value: str) -> OAuthToken:
1092
+ if ref.key == "oauth/openai-chatgpt":
1093
+ from pythinker_code.auth.openai import refresh_openai_chatgpt_token
1094
+
1095
+ return await refresh_openai_chatgpt_token(refresh_token_value)
1096
+ return await refresh_token(refresh_token_value)
1097
+
1098
+ def _apply_access_token(
1099
+ self, runtime: Runtime | None, ref: OAuthRef, access_token: str
1100
+ ) -> None:
1101
+ if runtime is None:
1102
+ return
1103
+ if runtime.llm is None or runtime.llm.model_config is None:
1104
+ return
1105
+ provider_key = runtime.llm.model_config.provider
1106
+ provider = runtime.config.providers.get(provider_key)
1107
+ if provider is None or provider.oauth != ref:
1108
+ return
1109
+ fallback_api_key = provider.api_key.get_secret_value()
1110
+ replacement = access_token or fallback_api_key
1111
+ chat_provider = cast(Any, runtime.llm.chat_provider)
1112
+ for client_attr in ("client", "_client"):
1113
+ client = getattr(chat_provider, client_attr, None)
1114
+ if client is not None and hasattr(client, "api_key"):
1115
+ client.api_key = replacement
1116
+ return
1117
+
1118
+
1119
+ if __name__ == "__main__":
1120
+ from rich import print
1121
+
1122
+ print(_common_headers())