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.
- pythinker_code/CHANGELOG.md +16 -0
- pythinker_code/__init__.py +0 -0
- pythinker_code/__main__.py +92 -0
- pythinker_code/acp/AGENTS.md +93 -0
- pythinker_code/acp/__init__.py +13 -0
- pythinker_code/acp/convert.py +128 -0
- pythinker_code/acp/host.py +298 -0
- pythinker_code/acp/mcp.py +46 -0
- pythinker_code/acp/server.py +497 -0
- pythinker_code/acp/session.py +496 -0
- pythinker_code/acp/tools.py +167 -0
- pythinker_code/acp/types.py +13 -0
- pythinker_code/acp/version.py +45 -0
- pythinker_code/agents/default/agent.yaml +36 -0
- pythinker_code/agents/default/coder.yaml +25 -0
- pythinker_code/agents/default/explore.yaml +46 -0
- pythinker_code/agents/default/plan.yaml +30 -0
- pythinker_code/agents/default/system.md +164 -0
- pythinker_code/agents/okabe/agent.yaml +22 -0
- pythinker_code/agentspec.py +163 -0
- pythinker_code/app.py +820 -0
- pythinker_code/approval_runtime/__init__.py +29 -0
- pythinker_code/approval_runtime/models.py +42 -0
- pythinker_code/approval_runtime/runtime.py +235 -0
- pythinker_code/auth/__init__.py +25 -0
- pythinker_code/auth/anthropic_direct.py +207 -0
- pythinker_code/auth/deepseek.py +192 -0
- pythinker_code/auth/lm_studio.py +418 -0
- pythinker_code/auth/minimax.py +203 -0
- pythinker_code/auth/oauth.py +1122 -0
- pythinker_code/auth/ollama.py +293 -0
- pythinker_code/auth/openai.py +771 -0
- pythinker_code/auth/opencode_go.py +203 -0
- pythinker_code/auth/openrouter.py +225 -0
- pythinker_code/auth/platforms.py +466 -0
- pythinker_code/background/__init__.py +36 -0
- pythinker_code/background/agent_runner.py +231 -0
- pythinker_code/background/ids.py +19 -0
- pythinker_code/background/manager.py +650 -0
- pythinker_code/background/models.py +105 -0
- pythinker_code/background/store.py +237 -0
- pythinker_code/background/summary.py +66 -0
- pythinker_code/background/worker.py +209 -0
- pythinker_code/cli/__init__.py +1326 -0
- pythinker_code/cli/__main__.py +19 -0
- pythinker_code/cli/_lazy_group.py +238 -0
- pythinker_code/cli/export.py +322 -0
- pythinker_code/cli/info.py +62 -0
- pythinker_code/cli/mcp.py +349 -0
- pythinker_code/cli/plugin.py +351 -0
- pythinker_code/cli/toad.py +74 -0
- pythinker_code/cli/vis.py +38 -0
- pythinker_code/cli/web.py +80 -0
- pythinker_code/config.py +453 -0
- pythinker_code/constant.py +33 -0
- pythinker_code/exception.py +43 -0
- pythinker_code/hooks/__init__.py +4 -0
- pythinker_code/hooks/config.py +34 -0
- pythinker_code/hooks/engine.py +371 -0
- pythinker_code/hooks/events.py +190 -0
- pythinker_code/hooks/runner.py +89 -0
- pythinker_code/llm.py +412 -0
- pythinker_code/metadata.py +79 -0
- pythinker_code/notifications/__init__.py +33 -0
- pythinker_code/notifications/llm.py +77 -0
- pythinker_code/notifications/manager.py +145 -0
- pythinker_code/notifications/models.py +50 -0
- pythinker_code/notifications/notifier.py +41 -0
- pythinker_code/notifications/store.py +118 -0
- pythinker_code/notifications/wire.py +21 -0
- pythinker_code/plugin/__init__.py +124 -0
- pythinker_code/plugin/manager.py +153 -0
- pythinker_code/plugin/tool.py +173 -0
- pythinker_code/prompts/__init__.py +6 -0
- pythinker_code/prompts/compact.md +73 -0
- pythinker_code/prompts/init.md +21 -0
- pythinker_code/py.typed +0 -0
- pythinker_code/session.py +319 -0
- pythinker_code/session_fork.py +325 -0
- pythinker_code/session_state.py +132 -0
- pythinker_code/share.py +14 -0
- pythinker_code/skill/__init__.py +727 -0
- pythinker_code/skill/flow/__init__.py +99 -0
- pythinker_code/skill/flow/d2.py +482 -0
- pythinker_code/skill/flow/mermaid.py +266 -0
- pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
- pythinker_code/skills/skill-creator/SKILL.md +367 -0
- pythinker_code/soul/__init__.py +304 -0
- pythinker_code/soul/agent.py +520 -0
- pythinker_code/soul/approval.py +267 -0
- pythinker_code/soul/btw.py +214 -0
- pythinker_code/soul/compaction.py +189 -0
- pythinker_code/soul/context.py +339 -0
- pythinker_code/soul/denwarenji.py +39 -0
- pythinker_code/soul/dynamic_injection.py +84 -0
- pythinker_code/soul/dynamic_injections/__init__.py +0 -0
- pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
- pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
- pythinker_code/soul/message.py +92 -0
- pythinker_code/soul/pythinkersoul.py +1613 -0
- pythinker_code/soul/slash.py +340 -0
- pythinker_code/soul/toolset.py +788 -0
- pythinker_code/subagents/__init__.py +21 -0
- pythinker_code/subagents/builder.py +42 -0
- pythinker_code/subagents/core.py +86 -0
- pythinker_code/subagents/git_context.py +172 -0
- pythinker_code/subagents/models.py +54 -0
- pythinker_code/subagents/output.py +71 -0
- pythinker_code/subagents/registry.py +28 -0
- pythinker_code/subagents/runner.py +428 -0
- pythinker_code/subagents/store.py +196 -0
- pythinker_code/telemetry/__init__.py +211 -0
- pythinker_code/telemetry/config.py +54 -0
- pythinker_code/telemetry/crash.py +157 -0
- pythinker_code/telemetry/metrics.py +208 -0
- pythinker_code/telemetry/otel.py +240 -0
- pythinker_code/telemetry/sentry.py +167 -0
- pythinker_code/telemetry/sink.py +189 -0
- pythinker_code/tools/AGENTS.md +6 -0
- pythinker_code/tools/__init__.py +105 -0
- pythinker_code/tools/agent/__init__.py +277 -0
- pythinker_code/tools/agent/description.md +41 -0
- pythinker_code/tools/ask_user/__init__.py +159 -0
- pythinker_code/tools/ask_user/description.md +19 -0
- pythinker_code/tools/background/__init__.py +318 -0
- pythinker_code/tools/background/list.md +10 -0
- pythinker_code/tools/background/output.md +11 -0
- pythinker_code/tools/background/stop.md +8 -0
- pythinker_code/tools/display.py +46 -0
- pythinker_code/tools/dmail/__init__.py +38 -0
- pythinker_code/tools/dmail/dmail.md +17 -0
- pythinker_code/tools/file/__init__.py +30 -0
- pythinker_code/tools/file/glob.md +17 -0
- pythinker_code/tools/file/glob.py +160 -0
- pythinker_code/tools/file/grep.md +6 -0
- pythinker_code/tools/file/grep_local.py +589 -0
- pythinker_code/tools/file/plan_mode.py +45 -0
- pythinker_code/tools/file/read.md +16 -0
- pythinker_code/tools/file/read.py +300 -0
- pythinker_code/tools/file/read_media.md +24 -0
- pythinker_code/tools/file/read_media.py +217 -0
- pythinker_code/tools/file/replace.md +7 -0
- pythinker_code/tools/file/replace.py +195 -0
- pythinker_code/tools/file/utils.py +257 -0
- pythinker_code/tools/file/write.md +5 -0
- pythinker_code/tools/file/write.py +177 -0
- pythinker_code/tools/plan/__init__.py +327 -0
- pythinker_code/tools/plan/description.md +29 -0
- pythinker_code/tools/plan/enter.py +190 -0
- pythinker_code/tools/plan/enter_description.md +35 -0
- pythinker_code/tools/plan/heroes.py +277 -0
- pythinker_code/tools/shell/__init__.py +253 -0
- pythinker_code/tools/shell/bash.md +35 -0
- pythinker_code/tools/shell/powershell.md +30 -0
- pythinker_code/tools/test.py +55 -0
- pythinker_code/tools/think/__init__.py +21 -0
- pythinker_code/tools/think/think.md +1 -0
- pythinker_code/tools/todo/__init__.py +168 -0
- pythinker_code/tools/todo/set_todo_list.md +23 -0
- pythinker_code/tools/utils.py +199 -0
- pythinker_code/tools/web/__init__.py +4 -0
- pythinker_code/tools/web/fetch.md +1 -0
- pythinker_code/tools/web/fetch.py +189 -0
- pythinker_code/tools/web/search.md +1 -0
- pythinker_code/tools/web/search.py +163 -0
- pythinker_code/ui/__init__.py +0 -0
- pythinker_code/ui/acp/__init__.py +99 -0
- pythinker_code/ui/print/__init__.py +474 -0
- pythinker_code/ui/print/visualize.py +185 -0
- pythinker_code/ui/shell/__init__.py +1696 -0
- pythinker_code/ui/shell/console.py +109 -0
- pythinker_code/ui/shell/debug.py +190 -0
- pythinker_code/ui/shell/echo.py +17 -0
- pythinker_code/ui/shell/export_import.py +117 -0
- pythinker_code/ui/shell/keyboard.py +300 -0
- pythinker_code/ui/shell/mcp_status.py +113 -0
- pythinker_code/ui/shell/model_picker.py +318 -0
- pythinker_code/ui/shell/oauth.py +272 -0
- pythinker_code/ui/shell/placeholders.py +531 -0
- pythinker_code/ui/shell/prompt.py +2278 -0
- pythinker_code/ui/shell/replay.py +215 -0
- pythinker_code/ui/shell/session_picker.py +227 -0
- pythinker_code/ui/shell/setup.py +212 -0
- pythinker_code/ui/shell/slash.py +898 -0
- pythinker_code/ui/shell/startup.py +32 -0
- pythinker_code/ui/shell/task_browser.py +486 -0
- pythinker_code/ui/shell/update.py +350 -0
- pythinker_code/ui/shell/usage.py +291 -0
- pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
- pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
- pythinker_code/ui/shell/usage_adapters/base.py +72 -0
- pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
- pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
- pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
- pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
- pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
- pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
- pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
- pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
- pythinker_code/ui/shell/usage_render.py +150 -0
- pythinker_code/ui/shell/visualize/__init__.py +165 -0
- pythinker_code/ui/shell/visualize/_approval_panel.py +505 -0
- pythinker_code/ui/shell/visualize/_blocks.py +629 -0
- pythinker_code/ui/shell/visualize/_btw_panel.py +224 -0
- pythinker_code/ui/shell/visualize/_input_router.py +48 -0
- pythinker_code/ui/shell/visualize/_interactive.py +523 -0
- pythinker_code/ui/shell/visualize/_live_view.py +826 -0
- pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
- pythinker_code/ui/theme.py +241 -0
- pythinker_code/usage_ratelimit_cache.py +175 -0
- pythinker_code/utils/__init__.py +0 -0
- pythinker_code/utils/aiohttp.py +24 -0
- pythinker_code/utils/aioqueue.py +72 -0
- pythinker_code/utils/broadcast.py +37 -0
- pythinker_code/utils/changelog.py +108 -0
- pythinker_code/utils/clipboard.py +246 -0
- pythinker_code/utils/datetime.py +64 -0
- pythinker_code/utils/diff.py +135 -0
- pythinker_code/utils/editor.py +91 -0
- pythinker_code/utils/environment.py +73 -0
- pythinker_code/utils/envvar.py +22 -0
- pythinker_code/utils/export.py +696 -0
- pythinker_code/utils/file_filter.py +375 -0
- pythinker_code/utils/frontmatter.py +70 -0
- pythinker_code/utils/io.py +27 -0
- pythinker_code/utils/logging.py +146 -0
- pythinker_code/utils/media_tags.py +29 -0
- pythinker_code/utils/message.py +24 -0
- pythinker_code/utils/path.py +199 -0
- pythinker_code/utils/proctitle.py +33 -0
- pythinker_code/utils/proxy.py +31 -0
- pythinker_code/utils/pyinstaller.py +45 -0
- pythinker_code/utils/rich/__init__.py +33 -0
- pythinker_code/utils/rich/columns.py +99 -0
- pythinker_code/utils/rich/diff_render.py +481 -0
- pythinker_code/utils/rich/markdown.py +900 -0
- pythinker_code/utils/rich/markdown_sample.md +108 -0
- pythinker_code/utils/rich/markdown_sample_short.md +2 -0
- pythinker_code/utils/rich/syntax.py +114 -0
- pythinker_code/utils/sensitive.py +54 -0
- pythinker_code/utils/server.py +121 -0
- pythinker_code/utils/signals.py +43 -0
- pythinker_code/utils/slashcmd.py +124 -0
- pythinker_code/utils/string.py +41 -0
- pythinker_code/utils/subprocess_env.py +73 -0
- pythinker_code/utils/term.py +168 -0
- pythinker_code/utils/typing.py +20 -0
- pythinker_code/vis/__init__.py +0 -0
- pythinker_code/vis/api/__init__.py +5 -0
- pythinker_code/vis/api/sessions.py +687 -0
- pythinker_code/vis/api/statistics.py +209 -0
- pythinker_code/vis/api/system.py +19 -0
- pythinker_code/vis/app.py +175 -0
- pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-D2MTYyJz.js +1 -0
- pythinker_code/vis/static/assets/index-CezafTt_.js +185 -0
- pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
- pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pythinker_code/vis/static/index.html +17 -0
- pythinker_code/web/__init__.py +5 -0
- pythinker_code/web/api/__init__.py +15 -0
- pythinker_code/web/api/config.py +208 -0
- pythinker_code/web/api/open_in.py +199 -0
- pythinker_code/web/api/sessions.py +1225 -0
- pythinker_code/web/app.py +451 -0
- pythinker_code/web/auth.py +191 -0
- pythinker_code/web/models.py +98 -0
- pythinker_code/web/runner/__init__.py +5 -0
- pythinker_code/web/runner/messages.py +57 -0
- pythinker_code/web/runner/process.py +754 -0
- pythinker_code/web/runner/worker.py +97 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- pythinker_code/web/static/assets/_baseUniq--dyU3g5v.js +1 -0
- pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
- pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
- pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
- pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
- pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
- pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
- pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
- pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
- pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
- pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
- pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
- pythinker_code/web/static/assets/arc-DkMjLpYa.js +1 -0
- pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-CHWVaGo9.js +36 -0
- pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
- pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
- pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
- pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
- pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
- pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
- pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
- pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
- pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
- pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
- pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
- pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
- pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
- pythinker_code/web/static/assets/blockDiagram-VD42YOAC-DzdKe497.js +122 -0
- pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
- pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
- pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
- pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-D84Blotg.js +10 -0
- pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
- pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
- pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- pythinker_code/web/static/assets/channel-CllSjjdl.js +1 -0
- pythinker_code/web/static/assets/chunk-4BX2VUAB-C9w8wleE.js +1 -0
- pythinker_code/web/static/assets/chunk-55IACEB6-YlYJ8HnF.js +1 -0
- pythinker_code/web/static/assets/chunk-B4BG7PRW-Bwtz_AHU.js +165 -0
- pythinker_code/web/static/assets/chunk-DI55MBZ5-BbEHkl8h.js +220 -0
- pythinker_code/web/static/assets/chunk-FMBD7UC4-BKPbvjLC.js +15 -0
- pythinker_code/web/static/assets/chunk-QN33PNHL-D73dQvKf.js +1 -0
- pythinker_code/web/static/assets/chunk-QZHKN3VN-zGiLKes_.js +1 -0
- pythinker_code/web/static/assets/chunk-TZMSLE5B-LHJCi2fy.js +1 -0
- pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-vX27iZwa.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-vX27iZwa.js +1 -0
- pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
- pythinker_code/web/static/assets/clone-DYBkaPm2.js +1 -0
- pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
- pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
- pythinker_code/web/static/assets/code-block-IT6T5CEO-NtKViZGl.js +2 -0
- pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
- pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
- pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
- pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
- pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
- pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-DialjZpd.js +1 -0
- pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
- pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
- pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
- pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
- pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
- pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
- pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
- pythinker_code/web/static/assets/cytoscape.esm-C_Fzpdck.js +321 -0
- pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
- pythinker_code/web/static/assets/dagre-6UL2VRFP-DfuvkZZ7.js +4 -0
- pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
- pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
- pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
- pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
- pythinker_code/web/static/assets/diagram-PSM6KHXK-DLGctX3v.js +24 -0
- pythinker_code/web/static/assets/diagram-QEK2KX5R-DnxN6S0F.js +43 -0
- pythinker_code/web/static/assets/diagram-S2PKOQOG-Caq_Set9.js +24 -0
- pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
- pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
- pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
- pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
- pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
- pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
- pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
- pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
- pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
- pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
- pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-BgTfALoK.js +60 -0
- pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
- pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
- pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
- pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
- pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
- pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
- pythinker_code/web/static/assets/flowDiagram-NV44I4VS-QjW_fnGi.js +162 -0
- pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
- pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
- pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
- pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-fqi8JFof.js +267 -0
- pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
- pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
- pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
- pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
- pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
- pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
- pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
- pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-i7o6VQ8x.js +65 -0
- pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
- pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
- pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
- pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
- pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
- pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
- pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
- pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
- pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
- pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
- pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
- pythinker_code/web/static/assets/graph-C0vZK2pT.js +1 -0
- pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
- pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
- pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
- pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
- pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
- pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
- pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
- pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
- pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
- pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
- pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
- pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
- pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
- pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
- pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
- pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
- pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
- pythinker_code/web/static/assets/index-BYCCk6-K.js +153 -0
- pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
- pythinker_code/web/static/assets/index-Cpy4G3uJ.js +2 -0
- pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
- pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
- pythinker_code/web/static/assets/index-DdIkp80K.js +5 -0
- pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-BMPpeZSM.js +2 -0
- pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
- pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
- pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
- pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
- pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
- pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
- pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-DAM7gngo.js +139 -0
- pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
- pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
- pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
- pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
- pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
- pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
- pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
- pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
- pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
- pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-ChpBpV0k.js +89 -0
- pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
- pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
- pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
- pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
- pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
- pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
- pythinker_code/web/static/assets/layout-C3Jp1gKO.js +1 -0
- pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
- pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
- pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
- pythinker_code/web/static/assets/linear-BGHtL1N4.js +1 -0
- pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
- pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
- pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
- pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
- pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
- pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
- pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
- pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
- pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
- pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
- pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
- pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
- pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
- pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
- pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
- pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
- pythinker_code/web/static/assets/mermaid-VLURNSYL-C_HW6koB.js +465 -0
- pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
- pythinker_code/web/static/assets/mermaid.core-CnT4VrPC.js +191 -0
- pythinker_code/web/static/assets/min-Dn5VRVmX.js +1 -0
- pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
- pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
- pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-x8EwhfIt.js +68 -0
- pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
- pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
- pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
- pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
- pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
- pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
- pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
- pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
- pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
- pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
- pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
- pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
- pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
- pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
- pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
- pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
- pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
- pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
- pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
- pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
- pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
- pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
- pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-DgxBKGz2.js +30 -0
- pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
- pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
- pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
- pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
- pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
- pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
- pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
- pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
- pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
- pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
- pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
- pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
- pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
- pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
- pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
- pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
- pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
- pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
- pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
- pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DSpe_dqk.js +7 -0
- pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
- pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
- pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
- pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
- pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
- pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
- pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
- pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
- pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-8o9hozL-.js +64 -0
- pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
- pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
- pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
- pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
- pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
- pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
- pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-BLOSUixH.js +10 -0
- pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
- pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
- pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
- pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
- pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
- pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
- pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-6F2G8JTU.js +145 -0
- pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
- pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
- pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
- pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
- pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
- pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
- pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
- pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
- pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
- pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
- pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
- pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
- pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
- pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
- pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
- pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DP8xP0iJ.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-1l6-EZNX.js +1 -0
- pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
- pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
- pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
- pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
- pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
- pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
- pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
- pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
- pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
- pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
- pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
- pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
- pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-DMgruDfK.js +61 -0
- pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
- pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
- pythinker_code/web/static/assets/treemap-KMMF4GRG-Bo9ZkrAK.js +128 -0
- pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
- pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
- pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
- pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
- pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
- pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
- pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
- pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
- pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
- pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
- pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
- pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
- pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
- pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
- pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
- pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
- pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
- pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
- pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
- pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
- pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
- pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
- pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
- pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
- pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
- pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
- pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
- pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
- pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
- pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
- pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
- pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-GeF2johi.js +7 -0
- pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
- pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
- pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
- pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
- pythinker_code/web/static/brand/arctecture.webp +0 -0
- pythinker_code/web/static/brand/bimi-logo.svg +46 -0
- pythinker_code/web/static/brand/favicon.ico +0 -0
- pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
- pythinker_code/web/static/brand/icon-192.png +0 -0
- pythinker_code/web/static/brand/icon-512.png +0 -0
- pythinker_code/web/static/brand/icon.svg +1 -0
- pythinker_code/web/static/brand/logo.png +0 -0
- pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
- pythinker_code/web/static/brand/robots.txt +4 -0
- pythinker_code/web/static/index.html +15 -0
- pythinker_code/web/static/logo.png +0 -0
- pythinker_code/web/store/__init__.py +1 -0
- pythinker_code/web/store/sessions.py +432 -0
- pythinker_code/wire/__init__.py +148 -0
- pythinker_code/wire/file.py +151 -0
- pythinker_code/wire/jsonrpc.py +263 -0
- pythinker_code/wire/protocol.py +2 -0
- pythinker_code/wire/root_hub.py +27 -0
- pythinker_code/wire/serde.py +26 -0
- pythinker_code/wire/server.py +1069 -0
- pythinker_code/wire/types.py +698 -0
- pythinker_code-2.0.0.dist-info/METADATA +660 -0
- pythinker_code-2.0.0.dist-info/RECORD +731 -0
- pythinker_code-2.0.0.dist-info/WHEEL +4 -0
- pythinker_code-2.0.0.dist-info/entry_points.txt +4 -0
- pythinker_code-2.0.0.dist-info/licenses/LICENSE +202 -0
- 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())
|