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,451 @@
|
|
|
1
|
+
"""Pythinker CLI Web UI application."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import secrets
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
|
|
13
|
+
import scalar_fastapi
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
17
|
+
from fastapi.staticfiles import StaticFiles
|
|
18
|
+
from starlette.datastructures import MutableHeaders
|
|
19
|
+
from starlette.responses import HTMLResponse
|
|
20
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
21
|
+
|
|
22
|
+
from pythinker_code.utils.logging import logger
|
|
23
|
+
from pythinker_code.utils.server import (
|
|
24
|
+
find_available_port,
|
|
25
|
+
format_url,
|
|
26
|
+
get_network_addresses,
|
|
27
|
+
is_local_host,
|
|
28
|
+
)
|
|
29
|
+
from pythinker_code.web.api import (
|
|
30
|
+
config_router,
|
|
31
|
+
open_in_router,
|
|
32
|
+
sessions_router,
|
|
33
|
+
work_dirs_router,
|
|
34
|
+
)
|
|
35
|
+
from pythinker_code.web.auth import (
|
|
36
|
+
DEFAULT_ALLOWED_ORIGIN_REGEX,
|
|
37
|
+
AuthMiddleware,
|
|
38
|
+
is_private_ip,
|
|
39
|
+
normalize_allowed_origins,
|
|
40
|
+
)
|
|
41
|
+
from pythinker_code.web.runner.process import PythinkerCLIRunner
|
|
42
|
+
|
|
43
|
+
# Configure logging based on LOG_LEVEL environment variable
|
|
44
|
+
_log_level = os.environ.get("LOG_LEVEL", "WARNING").upper()
|
|
45
|
+
logger.remove()
|
|
46
|
+
logger.enable("pythinker_code")
|
|
47
|
+
logger.add(sys.stderr, level=_log_level)
|
|
48
|
+
|
|
49
|
+
# scalar-fastapi does not ship typing stubs.
|
|
50
|
+
get_scalar_api_reference = cast( # pyright: ignore[reportUnknownMemberType]
|
|
51
|
+
Callable[..., HTMLResponse],
|
|
52
|
+
scalar_fastapi.get_scalar_api_reference, # pyright: ignore[reportUnknownMemberType]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Constants
|
|
56
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
57
|
+
GZIP_MINIMUM_SIZE = 1024
|
|
58
|
+
GZIP_COMPRESSION_LEVEL = 6
|
|
59
|
+
DEFAULT_PORT = 5494
|
|
60
|
+
MAX_PORT_ATTEMPTS = 10
|
|
61
|
+
ENV_SESSION_TOKEN = "PYTHINKER_WEB_SESSION_TOKEN"
|
|
62
|
+
ENV_ALLOWED_ORIGINS = "PYTHINKER_WEB_ALLOWED_ORIGINS"
|
|
63
|
+
ENV_ENFORCE_ORIGIN = "PYTHINKER_WEB_ENFORCE_ORIGIN"
|
|
64
|
+
ENV_RESTRICT_SENSITIVE_APIS = "PYTHINKER_WEB_RESTRICT_SENSITIVE_APIS"
|
|
65
|
+
ENV_MAX_PUBLIC_PATH_DEPTH = "PYTHINKER_WEB_MAX_PUBLIC_PATH_DEPTH"
|
|
66
|
+
|
|
67
|
+
# Cache durations
|
|
68
|
+
_IMMUTABLE_MAX_AGE = 365 * 24 * 3600 # 1 year for content-hashed assets
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _StaticCacheHeadersMiddleware:
|
|
72
|
+
"""Inject Cache-Control headers for static assets served by Starlette.
|
|
73
|
+
|
|
74
|
+
* ``index.html`` (and any non-hashed HTML) → ``no-cache`` so the browser
|
|
75
|
+
always revalidates, preventing stale references to renamed chunks after a
|
|
76
|
+
CLI upgrade (see #1602).
|
|
77
|
+
* Hashed assets under ``/assets/`` → long-lived ``immutable`` cache because
|
|
78
|
+
the content hash in the filename already guarantees uniqueness.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
82
|
+
self.app = app
|
|
83
|
+
|
|
84
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
85
|
+
if scope["type"] != "http":
|
|
86
|
+
await self.app(scope, receive, send)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
path: str = scope.get("path", "")
|
|
90
|
+
|
|
91
|
+
async def _send_with_cache_headers(message: Message) -> None:
|
|
92
|
+
if message["type"] == "http.response.start":
|
|
93
|
+
headers = MutableHeaders(scope=message)
|
|
94
|
+
if path.startswith("/assets/"):
|
|
95
|
+
headers["cache-control"] = f"public, max-age={_IMMUTABLE_MAX_AGE}, immutable"
|
|
96
|
+
elif path == "/" or path.endswith(".html"):
|
|
97
|
+
headers["cache-control"] = "no-cache, no-store, must-revalidate"
|
|
98
|
+
await send(message)
|
|
99
|
+
|
|
100
|
+
await self.app(scope, receive, _send_with_cache_headers)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_private_addresses(addresses: list[str]) -> list[str]:
|
|
104
|
+
"""Filter addresses to only include private IPs."""
|
|
105
|
+
return [ip for ip in addresses if is_private_ip(ip)]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _load_env_flag(key: str) -> bool:
|
|
109
|
+
return os.environ.get(key, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
ENV_LAN_ONLY = "PYTHINKER_WEB_LAN_ONLY"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_app(
|
|
116
|
+
session_token: str | None = None,
|
|
117
|
+
allowed_origins: list[str] | None = None,
|
|
118
|
+
enforce_origin: bool | None = None,
|
|
119
|
+
restrict_sensitive_apis: bool | None = None,
|
|
120
|
+
max_public_path_depth: int | None = None,
|
|
121
|
+
lan_only: bool | None = None,
|
|
122
|
+
) -> FastAPI:
|
|
123
|
+
"""Create the FastAPI application for Pythinker CLI web UI."""
|
|
124
|
+
|
|
125
|
+
env_token = os.environ.get(ENV_SESSION_TOKEN) or None
|
|
126
|
+
env_origins = normalize_allowed_origins(os.environ.get(ENV_ALLOWED_ORIGINS))
|
|
127
|
+
env_enforce_origin = _load_env_flag(ENV_ENFORCE_ORIGIN)
|
|
128
|
+
env_restrict_sensitive = _load_env_flag(ENV_RESTRICT_SENSITIVE_APIS)
|
|
129
|
+
env_max_depth_str = os.environ.get(ENV_MAX_PUBLIC_PATH_DEPTH)
|
|
130
|
+
env_max_depth = (
|
|
131
|
+
int(env_max_depth_str) if env_max_depth_str and env_max_depth_str.isdigit() else None
|
|
132
|
+
)
|
|
133
|
+
env_lan_only = _load_env_flag(ENV_LAN_ONLY)
|
|
134
|
+
|
|
135
|
+
session_token = session_token if session_token is not None else env_token
|
|
136
|
+
allowed_origins = allowed_origins if allowed_origins is not None else env_origins
|
|
137
|
+
enforce_origin = enforce_origin if enforce_origin is not None else env_enforce_origin
|
|
138
|
+
restrict_sensitive_apis = (
|
|
139
|
+
restrict_sensitive_apis if restrict_sensitive_apis is not None else env_restrict_sensitive
|
|
140
|
+
)
|
|
141
|
+
max_public_path_depth = (
|
|
142
|
+
max_public_path_depth if max_public_path_depth is not None else env_max_depth
|
|
143
|
+
)
|
|
144
|
+
lan_only = lan_only if lan_only is not None else env_lan_only
|
|
145
|
+
|
|
146
|
+
@asynccontextmanager
|
|
147
|
+
async def lifespan(app: FastAPI):
|
|
148
|
+
app.state.startup_dir = os.getcwd()
|
|
149
|
+
app.state.session_token = session_token
|
|
150
|
+
app.state.allowed_origins = allowed_origins
|
|
151
|
+
app.state.enforce_origin = enforce_origin
|
|
152
|
+
app.state.restrict_sensitive_apis = restrict_sensitive_apis
|
|
153
|
+
app.state.max_public_path_depth = max_public_path_depth
|
|
154
|
+
app.state.lan_only = lan_only
|
|
155
|
+
|
|
156
|
+
# Start PythinkerCLI runner
|
|
157
|
+
runner = PythinkerCLIRunner()
|
|
158
|
+
app.state.runner = runner
|
|
159
|
+
runner.start()
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
yield
|
|
163
|
+
finally:
|
|
164
|
+
await runner.stop()
|
|
165
|
+
|
|
166
|
+
application = FastAPI(
|
|
167
|
+
title="Pythinker CLI Web Interface",
|
|
168
|
+
docs_url=None,
|
|
169
|
+
lifespan=lifespan,
|
|
170
|
+
separate_input_output_schemas=False,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
application.add_middleware(
|
|
174
|
+
cast(Any, GZipMiddleware),
|
|
175
|
+
minimum_size=GZIP_MINIMUM_SIZE,
|
|
176
|
+
compresslevel=GZIP_COMPRESSION_LEVEL,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
application.add_middleware(cast(Any, _StaticCacheHeadersMiddleware))
|
|
180
|
+
|
|
181
|
+
application.add_middleware(
|
|
182
|
+
cast(Any, AuthMiddleware),
|
|
183
|
+
session_token=session_token,
|
|
184
|
+
allowed_origins=allowed_origins,
|
|
185
|
+
enforce_origin=enforce_origin,
|
|
186
|
+
lan_only=lan_only,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
cors_kwargs: dict[str, Any] = {
|
|
190
|
+
"allow_credentials": True,
|
|
191
|
+
"allow_methods": ["*"],
|
|
192
|
+
"allow_headers": ["*"],
|
|
193
|
+
}
|
|
194
|
+
if allowed_origins:
|
|
195
|
+
cors_kwargs["allow_origins"] = allowed_origins
|
|
196
|
+
else:
|
|
197
|
+
cors_kwargs["allow_origin_regex"] = DEFAULT_ALLOWED_ORIGIN_REGEX.pattern
|
|
198
|
+
|
|
199
|
+
# CORS middleware for local development
|
|
200
|
+
application.add_middleware(cast(Any, CORSMiddleware), **cors_kwargs)
|
|
201
|
+
|
|
202
|
+
application.include_router(config_router)
|
|
203
|
+
application.include_router(sessions_router)
|
|
204
|
+
application.include_router(work_dirs_router)
|
|
205
|
+
if not restrict_sensitive_apis:
|
|
206
|
+
application.include_router(open_in_router)
|
|
207
|
+
|
|
208
|
+
@application.get("/scalar", include_in_schema=False)
|
|
209
|
+
@application.get("/docs", include_in_schema=False)
|
|
210
|
+
async def scalar_html() -> HTMLResponse: # pyright: ignore[reportUnusedFunction]
|
|
211
|
+
return get_scalar_api_reference(
|
|
212
|
+
openapi_url=application.openapi_url or "",
|
|
213
|
+
title=application.title,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@application.get("/healthz")
|
|
217
|
+
async def health_probe() -> dict[str, Any]: # pyright: ignore[reportUnusedFunction]
|
|
218
|
+
"""Health check endpoint."""
|
|
219
|
+
return {"status": "ok"}
|
|
220
|
+
|
|
221
|
+
# Mount static files as fallback (must be last)
|
|
222
|
+
if STATIC_DIR.exists():
|
|
223
|
+
application.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")
|
|
224
|
+
|
|
225
|
+
return application
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def run_web_server(
|
|
229
|
+
host: str = "127.0.0.1",
|
|
230
|
+
port: int = DEFAULT_PORT,
|
|
231
|
+
reload: bool = False,
|
|
232
|
+
open_browser: bool = True,
|
|
233
|
+
auth_token: str | None = None,
|
|
234
|
+
allowed_origins: str | None = None,
|
|
235
|
+
dangerously_omit_auth: bool = False,
|
|
236
|
+
restrict_sensitive_apis: bool | None = None,
|
|
237
|
+
lan_only: bool = True,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Run the web server."""
|
|
240
|
+
import sys
|
|
241
|
+
import threading
|
|
242
|
+
|
|
243
|
+
import uvicorn
|
|
244
|
+
|
|
245
|
+
from pythinker_code.utils.server import print_banner
|
|
246
|
+
|
|
247
|
+
public_mode = not is_local_host(host)
|
|
248
|
+
parsed_allowed_origins = normalize_allowed_origins(allowed_origins)
|
|
249
|
+
auto_populate_origins = public_mode and not parsed_allowed_origins
|
|
250
|
+
|
|
251
|
+
if restrict_sensitive_apis is None:
|
|
252
|
+
# Only restrict sensitive APIs in public mode (non-LAN-only)
|
|
253
|
+
restrict_sensitive_apis = public_mode and not lan_only
|
|
254
|
+
|
|
255
|
+
if public_mode and dangerously_omit_auth:
|
|
256
|
+
warning_lines = [
|
|
257
|
+
"SECURITY WARNING",
|
|
258
|
+
"",
|
|
259
|
+
"Authentication is DISABLED while running on a public host.",
|
|
260
|
+
"Anyone on the network can access your sessions and files.",
|
|
261
|
+
"",
|
|
262
|
+
"Type 'I UNDERSTAND THE RISKS' to continue:",
|
|
263
|
+
]
|
|
264
|
+
print_banner(warning_lines)
|
|
265
|
+
if not sys.stdin.isatty():
|
|
266
|
+
raise RuntimeError("Refusing to start without auth in non-interactive mode.")
|
|
267
|
+
response = input("> ").strip()
|
|
268
|
+
if response != "I UNDERSTAND THE RISKS":
|
|
269
|
+
raise RuntimeError("Aborted by user.")
|
|
270
|
+
|
|
271
|
+
if dangerously_omit_auth:
|
|
272
|
+
session_token = None
|
|
273
|
+
elif auth_token:
|
|
274
|
+
session_token = auth_token
|
|
275
|
+
elif public_mode:
|
|
276
|
+
session_token = secrets.token_urlsafe(32)
|
|
277
|
+
else:
|
|
278
|
+
session_token = None
|
|
279
|
+
|
|
280
|
+
if session_token:
|
|
281
|
+
os.environ[ENV_SESSION_TOKEN] = session_token
|
|
282
|
+
else:
|
|
283
|
+
os.environ.pop(ENV_SESSION_TOKEN, None)
|
|
284
|
+
|
|
285
|
+
# Find available port first (needed for auto-populating origins)
|
|
286
|
+
actual_port = find_available_port(host, port)
|
|
287
|
+
if actual_port != port:
|
|
288
|
+
print(f"Port {port} is in use, using port {actual_port} instead")
|
|
289
|
+
|
|
290
|
+
# Auto-populate allowed origins with detected network addresses + port
|
|
291
|
+
if auto_populate_origins:
|
|
292
|
+
auto_origins = [
|
|
293
|
+
f"http://localhost:{actual_port}",
|
|
294
|
+
f"http://127.0.0.1:{actual_port}",
|
|
295
|
+
]
|
|
296
|
+
if host == "0.0.0.0":
|
|
297
|
+
# Binding to all interfaces: add all network addresses
|
|
298
|
+
network_addrs = get_network_addresses()
|
|
299
|
+
for addr in network_addrs:
|
|
300
|
+
auto_origins.append(format_url(addr, actual_port))
|
|
301
|
+
else:
|
|
302
|
+
# Explicit host specified: only add that host
|
|
303
|
+
auto_origins.append(format_url(host, actual_port))
|
|
304
|
+
parsed_allowed_origins = auto_origins
|
|
305
|
+
|
|
306
|
+
if parsed_allowed_origins:
|
|
307
|
+
os.environ[ENV_ALLOWED_ORIGINS] = ",".join(parsed_allowed_origins)
|
|
308
|
+
else:
|
|
309
|
+
os.environ.pop(ENV_ALLOWED_ORIGINS, None)
|
|
310
|
+
|
|
311
|
+
os.environ[ENV_ENFORCE_ORIGIN] = "1" if (public_mode and not lan_only) else "0"
|
|
312
|
+
os.environ[ENV_RESTRICT_SENSITIVE_APIS] = "1" if restrict_sensitive_apis else "0"
|
|
313
|
+
os.environ[ENV_LAN_ONLY] = "1" if lan_only else "0"
|
|
314
|
+
|
|
315
|
+
# Determine display URLs
|
|
316
|
+
display_hosts: list[tuple[str, str]] = []
|
|
317
|
+
if host == "0.0.0.0":
|
|
318
|
+
# Show localhost as "Local" and network interfaces
|
|
319
|
+
display_hosts.append(("Local", "localhost"))
|
|
320
|
+
network_addrs = get_network_addresses()
|
|
321
|
+
|
|
322
|
+
# In lan_only mode, only show private IPs
|
|
323
|
+
if lan_only:
|
|
324
|
+
network_addrs = _get_private_addresses(network_addrs)
|
|
325
|
+
|
|
326
|
+
for addr in network_addrs:
|
|
327
|
+
display_hosts.append(("Network", addr))
|
|
328
|
+
else:
|
|
329
|
+
# Show the specified host
|
|
330
|
+
label = "Local" if is_local_host(host) else "Network"
|
|
331
|
+
display_hosts.append((label, host))
|
|
332
|
+
|
|
333
|
+
# Build URLs with token if needed
|
|
334
|
+
def make_url(host_addr: str) -> tuple[str, str]:
|
|
335
|
+
"""Returns (url, browser_url) tuple."""
|
|
336
|
+
url = format_url(host_addr, actual_port)
|
|
337
|
+
browser_url = f"{url}/?token={quote(session_token)}" if session_token else url
|
|
338
|
+
return url, browser_url
|
|
339
|
+
|
|
340
|
+
# For browser opening, prefer localhost, then first network address
|
|
341
|
+
browser_host = "localhost" if host == "0.0.0.0" else host
|
|
342
|
+
_, browser_url = make_url(browser_host)
|
|
343
|
+
|
|
344
|
+
if open_browser:
|
|
345
|
+
|
|
346
|
+
def open_browser_after_delay():
|
|
347
|
+
import time
|
|
348
|
+
|
|
349
|
+
time.sleep(1.5)
|
|
350
|
+
webbrowser.open(browser_url)
|
|
351
|
+
|
|
352
|
+
# Start browser opener in a daemon thread
|
|
353
|
+
thread = threading.Thread(target=open_browser_after_delay, daemon=True)
|
|
354
|
+
thread.start()
|
|
355
|
+
|
|
356
|
+
banner_lines = [
|
|
357
|
+
"<center>██╗ ██╗██╗███╗ ███╗██╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
358
|
+
"<center>██║ ██╔╝██║████╗ ████║██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
359
|
+
"<center>█████╔╝ ██║██╔████╔██║██║ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
360
|
+
"<center>██╔═██╗ ██║██║╚██╔╝██║██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
361
|
+
"<center>██║ ██╗██║██║ ╚═╝ ██║██║ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
362
|
+
"<center>╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
363
|
+
"",
|
|
364
|
+
"<center>WEB UI (Technical Preview)",
|
|
365
|
+
"",
|
|
366
|
+
"<hr>",
|
|
367
|
+
"",
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
# Add URLs for each host (nowrap to keep URLs on single line for easy copying)
|
|
371
|
+
for label, host_addr in display_hosts:
|
|
372
|
+
url, url_with_token = make_url(host_addr)
|
|
373
|
+
if session_token:
|
|
374
|
+
banner_lines.append(f"<nowrap> ➜ {label:8} {url_with_token}")
|
|
375
|
+
else:
|
|
376
|
+
banner_lines.append(f"<nowrap> ➜ {label:8} {url}")
|
|
377
|
+
|
|
378
|
+
# Auth token or warnings
|
|
379
|
+
if session_token:
|
|
380
|
+
banner_lines.extend(
|
|
381
|
+
[
|
|
382
|
+
"",
|
|
383
|
+
f"<nowrap> Token: {session_token}",
|
|
384
|
+
]
|
|
385
|
+
)
|
|
386
|
+
elif public_mode:
|
|
387
|
+
banner_lines.extend(
|
|
388
|
+
[
|
|
389
|
+
"",
|
|
390
|
+
"<nowrap> ⚠ AUTH DISABLED - Anyone on the network can access",
|
|
391
|
+
]
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if restrict_sensitive_apis:
|
|
395
|
+
banner_lines.append("<nowrap> ⚠ Sensitive APIs are restricted")
|
|
396
|
+
|
|
397
|
+
# Show network access mode and tips
|
|
398
|
+
banner_lines.append("")
|
|
399
|
+
banner_lines.append("<hr>")
|
|
400
|
+
banner_lines.append("")
|
|
401
|
+
|
|
402
|
+
if not public_mode:
|
|
403
|
+
# Local-only mode (127.0.0.1)
|
|
404
|
+
banner_lines.extend(
|
|
405
|
+
[
|
|
406
|
+
"<nowrap> Tips:",
|
|
407
|
+
"<nowrap> • Use -n / --network to share on LAN",
|
|
408
|
+
"<nowrap> • Use --network --public for public access",
|
|
409
|
+
]
|
|
410
|
+
)
|
|
411
|
+
elif lan_only:
|
|
412
|
+
# LAN mode (0.0.0.0 with lan_only)
|
|
413
|
+
banner_lines.extend(
|
|
414
|
+
[
|
|
415
|
+
"<nowrap> Mode: LAN only (private IPs)",
|
|
416
|
+
"",
|
|
417
|
+
"<nowrap> Tips:",
|
|
418
|
+
"<nowrap> • Use --public to allow public access",
|
|
419
|
+
"<nowrap> • ⚠ Public mode allows access from any IP",
|
|
420
|
+
]
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
# Public mode (0.0.0.0 without lan_only)
|
|
424
|
+
banner_lines.extend(
|
|
425
|
+
[
|
|
426
|
+
"<nowrap> ⚠ Mode: PUBLIC (all networks)",
|
|
427
|
+
"<nowrap> Anyone with the URL can access this instance",
|
|
428
|
+
"",
|
|
429
|
+
"<nowrap> Security tips:",
|
|
430
|
+
"<nowrap> • Keep your auth token secure",
|
|
431
|
+
"<nowrap> • Consider using firewall or VPN",
|
|
432
|
+
]
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
banner_lines.append("")
|
|
436
|
+
|
|
437
|
+
print_banner(banner_lines)
|
|
438
|
+
# print(f"API docs available at {url}/docs")
|
|
439
|
+
|
|
440
|
+
uvicorn.run(
|
|
441
|
+
"pythinker_code.web.app:create_app",
|
|
442
|
+
factory=True,
|
|
443
|
+
host=host,
|
|
444
|
+
port=actual_port,
|
|
445
|
+
reload=reload,
|
|
446
|
+
log_level="info",
|
|
447
|
+
timeout_graceful_shutdown=3,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
__all__ = ["create_app", "run_web_server"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Auth helpers and middleware for Pythinker CLI web."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hmac
|
|
6
|
+
import ipaddress
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
|
|
10
|
+
from fastapi import Request
|
|
11
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
12
|
+
from starlette.responses import JSONResponse
|
|
13
|
+
from starlette.types import ASGIApp
|
|
14
|
+
|
|
15
|
+
DEFAULT_ALLOWED_ORIGIN_REGEX = re.compile(r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def timing_safe_compare(a: str, b: str) -> bool:
|
|
19
|
+
"""Timing-safe string comparison."""
|
|
20
|
+
return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_bearer_token(value: str | None) -> str | None:
|
|
24
|
+
"""Extract bearer token from Authorization header."""
|
|
25
|
+
if not value:
|
|
26
|
+
return None
|
|
27
|
+
scheme, _, token = value.partition(" ")
|
|
28
|
+
if scheme.lower() != "bearer":
|
|
29
|
+
return None
|
|
30
|
+
token = token.strip()
|
|
31
|
+
return token or None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_allowed_origins(value: str | None) -> list[str]:
|
|
35
|
+
"""Parse comma-separated origins into a normalized list."""
|
|
36
|
+
if not value:
|
|
37
|
+
return []
|
|
38
|
+
origins: list[str] = []
|
|
39
|
+
for raw in value.split(","):
|
|
40
|
+
origin = raw.strip().rstrip("/")
|
|
41
|
+
if origin:
|
|
42
|
+
origins.append(origin)
|
|
43
|
+
return origins
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_origin_allowed(origin: str, allowed_origins: Iterable[str] | None) -> bool:
|
|
47
|
+
"""Check if an origin is allowed.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
origin: The origin to check
|
|
51
|
+
allowed_origins: List of allowed origins.
|
|
52
|
+
- None: use default localhost regex
|
|
53
|
+
- Empty list: reject all origins
|
|
54
|
+
- Non-empty list: check against the list (supports "*" wildcard)
|
|
55
|
+
"""
|
|
56
|
+
origin = origin.rstrip("/")
|
|
57
|
+
|
|
58
|
+
# None means use default behavior (localhost only)
|
|
59
|
+
if allowed_origins is None:
|
|
60
|
+
return bool(DEFAULT_ALLOWED_ORIGIN_REGEX.match(origin))
|
|
61
|
+
|
|
62
|
+
allowed = list(allowed_origins)
|
|
63
|
+
|
|
64
|
+
# Empty list explicitly means reject all
|
|
65
|
+
if not allowed:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Check for wildcard or exact match
|
|
69
|
+
if "*" in allowed:
|
|
70
|
+
return True
|
|
71
|
+
return origin in allowed
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_token_from_request(request: Request) -> str | None:
|
|
75
|
+
"""Get auth token from Authorization header or query (GET-only)."""
|
|
76
|
+
token = parse_bearer_token(request.headers.get("authorization"))
|
|
77
|
+
if token:
|
|
78
|
+
return token
|
|
79
|
+
if request.method.upper() == "GET":
|
|
80
|
+
query_token = request.query_params.get("token")
|
|
81
|
+
if query_token:
|
|
82
|
+
return query_token
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def verify_token(provided: str | None, expected: str) -> bool:
|
|
87
|
+
"""Verify token using timing-safe comparison."""
|
|
88
|
+
if not provided:
|
|
89
|
+
return False
|
|
90
|
+
return timing_safe_compare(provided, expected)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_private_ip(ip: str) -> bool:
|
|
94
|
+
"""Check if an IP address is in a private range (RFC 1918 + localhost).
|
|
95
|
+
|
|
96
|
+
Supports both IPv4 and IPv6 addresses.
|
|
97
|
+
"""
|
|
98
|
+
if not ip:
|
|
99
|
+
return False
|
|
100
|
+
try:
|
|
101
|
+
addr = ipaddress.ip_address(ip)
|
|
102
|
+
# is_private covers RFC 1918 (10.x, 172.16-31.x, 192.168.x)
|
|
103
|
+
# is_loopback covers 127.x.x.x and ::1
|
|
104
|
+
# is_link_local covers 169.254.x.x and fe80::/10
|
|
105
|
+
return addr.is_private or addr.is_loopback or addr.is_link_local
|
|
106
|
+
except ValueError:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_client_ip(request: Request, trust_proxy: bool = False) -> str | None:
|
|
111
|
+
"""Extract client IP from request.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
request: The incoming request
|
|
115
|
+
trust_proxy: If True, trust X-Forwarded-For header (only enable behind trusted proxy)
|
|
116
|
+
"""
|
|
117
|
+
if trust_proxy:
|
|
118
|
+
forwarded = request.headers.get("x-forwarded-for")
|
|
119
|
+
if forwarded:
|
|
120
|
+
return forwarded.split(",")[0].strip()
|
|
121
|
+
if request.client:
|
|
122
|
+
return request.client.host
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AuthMiddleware(BaseHTTPMiddleware):
|
|
127
|
+
"""Bearer token auth, origin checks, and LAN-only mode for API routes."""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
app: ASGIApp,
|
|
132
|
+
session_token: str | None,
|
|
133
|
+
allowed_origins: Iterable[str] | None,
|
|
134
|
+
enforce_origin: bool,
|
|
135
|
+
lan_only: bool = False,
|
|
136
|
+
) -> None:
|
|
137
|
+
super().__init__(app)
|
|
138
|
+
self._session_token = session_token
|
|
139
|
+
self._allowed_origins = list(allowed_origins) if allowed_origins is not None else None
|
|
140
|
+
self._enforce_origin = enforce_origin
|
|
141
|
+
self._lan_only = lan_only
|
|
142
|
+
|
|
143
|
+
async def dispatch(self, request: Request, call_next): # type: ignore[override]
|
|
144
|
+
path = request.url.path
|
|
145
|
+
|
|
146
|
+
# LAN-only check applies to all requests (including static files)
|
|
147
|
+
if self._lan_only:
|
|
148
|
+
client_ip = get_client_ip(request)
|
|
149
|
+
if client_ip and not is_private_ip(client_ip):
|
|
150
|
+
return JSONResponse(
|
|
151
|
+
status_code=403,
|
|
152
|
+
content={"detail": "Access denied: only local network access is allowed"},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if request.method.upper() == "OPTIONS":
|
|
156
|
+
return await call_next(request)
|
|
157
|
+
if path in {"/healthz", "/docs", "/scalar"}:
|
|
158
|
+
return await call_next(request)
|
|
159
|
+
if not path.startswith("/api/"):
|
|
160
|
+
return await call_next(request)
|
|
161
|
+
|
|
162
|
+
if self._enforce_origin:
|
|
163
|
+
origin = request.headers.get("origin")
|
|
164
|
+
if origin and not is_origin_allowed(origin, self._allowed_origins):
|
|
165
|
+
return JSONResponse(
|
|
166
|
+
status_code=403,
|
|
167
|
+
content={"detail": "Origin not allowed"},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if self._session_token:
|
|
171
|
+
provided = extract_token_from_request(request)
|
|
172
|
+
if not verify_token(provided, self._session_token):
|
|
173
|
+
return JSONResponse(
|
|
174
|
+
status_code=401,
|
|
175
|
+
content={"detail": "Unauthorized"},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return await call_next(request)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
__all__ = [
|
|
182
|
+
"AuthMiddleware",
|
|
183
|
+
"DEFAULT_ALLOWED_ORIGIN_REGEX",
|
|
184
|
+
"extract_token_from_request",
|
|
185
|
+
"get_client_ip",
|
|
186
|
+
"is_origin_allowed",
|
|
187
|
+
"is_private_ip",
|
|
188
|
+
"normalize_allowed_origins",
|
|
189
|
+
"timing_safe_compare",
|
|
190
|
+
"verify_token",
|
|
191
|
+
]
|