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,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pythinker_code.config import NotificationConfig
|
|
9
|
+
from pythinker_code.utils.logging import logger
|
|
10
|
+
|
|
11
|
+
from .models import (
|
|
12
|
+
NotificationDelivery,
|
|
13
|
+
NotificationEvent,
|
|
14
|
+
NotificationSinkState,
|
|
15
|
+
NotificationView,
|
|
16
|
+
)
|
|
17
|
+
from .store import NotificationStore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotificationManager:
|
|
21
|
+
def __init__(self, root: Path, config: NotificationConfig) -> None:
|
|
22
|
+
self._config = config
|
|
23
|
+
self._store = NotificationStore(root)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def store(self) -> NotificationStore:
|
|
27
|
+
return self._store
|
|
28
|
+
|
|
29
|
+
def new_id(self) -> str:
|
|
30
|
+
return f"n{uuid.uuid4().hex[:8]}"
|
|
31
|
+
|
|
32
|
+
def _initial_delivery(self, event: NotificationEvent) -> NotificationDelivery:
|
|
33
|
+
return NotificationDelivery(sinks={sink: NotificationSinkState() for sink in event.targets})
|
|
34
|
+
|
|
35
|
+
def find_by_dedupe_key(self, dedupe_key: str) -> NotificationView | None:
|
|
36
|
+
for view in self._store.list_views():
|
|
37
|
+
if view.event.dedupe_key == dedupe_key:
|
|
38
|
+
return view
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def publish(self, event: NotificationEvent) -> NotificationView:
|
|
42
|
+
if event.dedupe_key:
|
|
43
|
+
existing = self.find_by_dedupe_key(event.dedupe_key)
|
|
44
|
+
if existing is not None:
|
|
45
|
+
return existing
|
|
46
|
+
delivery = self._initial_delivery(event)
|
|
47
|
+
self._store.create_notification(event, delivery)
|
|
48
|
+
return NotificationView(event=event, delivery=delivery)
|
|
49
|
+
|
|
50
|
+
def recover(self) -> None:
|
|
51
|
+
now = time.time()
|
|
52
|
+
stale_after = self._config.claim_stale_after_ms / 1000
|
|
53
|
+
for view in self._store.list_views():
|
|
54
|
+
updated = False
|
|
55
|
+
delivery = view.delivery.model_copy(deep=True)
|
|
56
|
+
for sink_state in delivery.sinks.values():
|
|
57
|
+
if sink_state.status != "claimed" or sink_state.claimed_at is None:
|
|
58
|
+
continue
|
|
59
|
+
if now - sink_state.claimed_at <= stale_after:
|
|
60
|
+
continue
|
|
61
|
+
sink_state.status = "pending"
|
|
62
|
+
sink_state.claimed_at = None
|
|
63
|
+
updated = True
|
|
64
|
+
if updated:
|
|
65
|
+
self._store.write_delivery(view.event.id, delivery)
|
|
66
|
+
|
|
67
|
+
def has_pending_for_sink(self, sink: str) -> bool:
|
|
68
|
+
"""Check whether any notification has a pending delivery for *sink*."""
|
|
69
|
+
for view in self._store.list_views():
|
|
70
|
+
sink_state = view.delivery.sinks.get(sink)
|
|
71
|
+
if sink_state is not None and sink_state.status == "pending":
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def claim_for_sink(self, sink: str, *, limit: int = 8) -> list[NotificationView]:
|
|
76
|
+
self.recover()
|
|
77
|
+
claimed: list[NotificationView] = []
|
|
78
|
+
now = time.time()
|
|
79
|
+
for view in reversed(self._store.list_views()):
|
|
80
|
+
sink_state = view.delivery.sinks.get(sink)
|
|
81
|
+
if sink_state is None or sink_state.status == "acked":
|
|
82
|
+
continue
|
|
83
|
+
if sink_state.status == "claimed":
|
|
84
|
+
continue
|
|
85
|
+
delivery = view.delivery.model_copy(deep=True)
|
|
86
|
+
target_state = delivery.sinks[sink]
|
|
87
|
+
target_state.status = "claimed"
|
|
88
|
+
target_state.claimed_at = now
|
|
89
|
+
self._store.write_delivery(view.event.id, delivery)
|
|
90
|
+
claimed.append(NotificationView(event=view.event, delivery=delivery))
|
|
91
|
+
if len(claimed) >= limit:
|
|
92
|
+
break
|
|
93
|
+
return claimed
|
|
94
|
+
|
|
95
|
+
async def deliver_pending(
|
|
96
|
+
self,
|
|
97
|
+
sink: str,
|
|
98
|
+
*,
|
|
99
|
+
on_notification: Callable[[NotificationView], Awaitable[None] | None],
|
|
100
|
+
limit: int = 8,
|
|
101
|
+
before_claim: Callable[[], object] | None = None,
|
|
102
|
+
) -> list[NotificationView]:
|
|
103
|
+
"""Deliver pending notifications for one sink using a shared claim/ack flow.
|
|
104
|
+
|
|
105
|
+
If the handler raises for a notification, the error is logged and that
|
|
106
|
+
notification stays in ``claimed`` state (will be recovered later).
|
|
107
|
+
Delivery continues for remaining notifications.
|
|
108
|
+
"""
|
|
109
|
+
if before_claim is not None:
|
|
110
|
+
before_claim()
|
|
111
|
+
|
|
112
|
+
delivered: list[NotificationView] = []
|
|
113
|
+
for view in self.claim_for_sink(sink, limit=limit):
|
|
114
|
+
try:
|
|
115
|
+
result = on_notification(view)
|
|
116
|
+
if result is not None:
|
|
117
|
+
await result
|
|
118
|
+
except Exception:
|
|
119
|
+
logger.exception(
|
|
120
|
+
"Notification handler failed for {sink}/{id}, leaving claimed for recovery",
|
|
121
|
+
sink=sink,
|
|
122
|
+
id=view.event.id,
|
|
123
|
+
)
|
|
124
|
+
continue
|
|
125
|
+
delivered.append(self.ack(sink, view.event.id))
|
|
126
|
+
return delivered
|
|
127
|
+
|
|
128
|
+
def ack(self, sink: str, notification_id: str) -> NotificationView:
|
|
129
|
+
view = self._store.merged_view(notification_id)
|
|
130
|
+
delivery = view.delivery.model_copy(deep=True)
|
|
131
|
+
sink_state = delivery.sinks.get(sink)
|
|
132
|
+
if sink_state is None:
|
|
133
|
+
return view
|
|
134
|
+
sink_state.status = "acked"
|
|
135
|
+
sink_state.acked_at = time.time()
|
|
136
|
+
sink_state.claimed_at = None
|
|
137
|
+
self._store.write_delivery(notification_id, delivery)
|
|
138
|
+
return NotificationView(event=view.event, delivery=delivery)
|
|
139
|
+
|
|
140
|
+
def ack_ids(self, sink: str, notification_ids: set[str]) -> None:
|
|
141
|
+
for notification_id in notification_ids:
|
|
142
|
+
try:
|
|
143
|
+
self.ack(sink, notification_id)
|
|
144
|
+
except (FileNotFoundError, ValueError):
|
|
145
|
+
continue
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
type NotificationCategory = Literal["task", "agent", "system"]
|
|
9
|
+
type NotificationSeverity = Literal["info", "success", "warning", "error"]
|
|
10
|
+
type NotificationSink = Literal["llm", "wire", "shell"]
|
|
11
|
+
type NotificationDeliveryStatus = Literal["pending", "claimed", "acked"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NotificationEvent(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="ignore")
|
|
16
|
+
|
|
17
|
+
version: int = 1
|
|
18
|
+
id: str
|
|
19
|
+
category: NotificationCategory
|
|
20
|
+
type: str
|
|
21
|
+
source_kind: str
|
|
22
|
+
source_id: str
|
|
23
|
+
title: str
|
|
24
|
+
body: str
|
|
25
|
+
severity: NotificationSeverity = "info"
|
|
26
|
+
created_at: float = Field(default_factory=time.time)
|
|
27
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
28
|
+
targets: list[NotificationSink] = Field(default_factory=lambda: ["llm", "wire", "shell"])
|
|
29
|
+
dedupe_key: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NotificationSinkState(BaseModel):
|
|
33
|
+
model_config = ConfigDict(extra="ignore")
|
|
34
|
+
|
|
35
|
+
status: NotificationDeliveryStatus = "pending"
|
|
36
|
+
claimed_at: float | None = None
|
|
37
|
+
acked_at: float | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NotificationDelivery(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra="ignore")
|
|
42
|
+
|
|
43
|
+
sinks: dict[str, NotificationSinkState] = Field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NotificationView(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="ignore")
|
|
48
|
+
|
|
49
|
+
event: NotificationEvent
|
|
50
|
+
delivery: NotificationDelivery
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
3
|
+
|
|
4
|
+
from pythinker_code.utils.logging import logger
|
|
5
|
+
|
|
6
|
+
from .manager import NotificationManager
|
|
7
|
+
from .models import NotificationSink, NotificationView
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NotificationWatcher:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
manager: NotificationManager,
|
|
14
|
+
*,
|
|
15
|
+
sink: NotificationSink,
|
|
16
|
+
on_notification: Callable[[NotificationView], Awaitable[None] | None],
|
|
17
|
+
before_poll: Callable[[], object] | None = None,
|
|
18
|
+
interval_s: float = 1.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._manager = manager
|
|
21
|
+
self._sink = sink
|
|
22
|
+
self._on_notification = on_notification
|
|
23
|
+
self._before_poll = before_poll
|
|
24
|
+
self._interval_s = interval_s
|
|
25
|
+
|
|
26
|
+
async def poll_once(self) -> list[NotificationView]:
|
|
27
|
+
return await self._manager.deliver_pending(
|
|
28
|
+
self._sink,
|
|
29
|
+
on_notification=self._on_notification,
|
|
30
|
+
before_claim=self._before_poll,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def run_forever(self) -> None:
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
await self.poll_once()
|
|
37
|
+
except asyncio.CancelledError:
|
|
38
|
+
raise
|
|
39
|
+
except Exception:
|
|
40
|
+
logger.exception("NotificationWatcher poll failed")
|
|
41
|
+
await asyncio.sleep(self._interval_s)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from pythinker_code.utils.io import atomic_json_write
|
|
9
|
+
from pythinker_code.utils.logging import logger
|
|
10
|
+
|
|
11
|
+
from .models import NotificationDelivery, NotificationEvent, NotificationView
|
|
12
|
+
|
|
13
|
+
_VALID_NOTIFICATION_ID = re.compile(r"^[a-z0-9]{2,20}$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_notification_id(notification_id: str) -> None:
|
|
17
|
+
if not _VALID_NOTIFICATION_ID.match(notification_id):
|
|
18
|
+
raise ValueError(f"Invalid notification_id: {notification_id!r}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotificationStore:
|
|
22
|
+
EVENT_FILE = "event.json"
|
|
23
|
+
DELIVERY_FILE = "delivery.json"
|
|
24
|
+
|
|
25
|
+
def __init__(self, root: Path):
|
|
26
|
+
self._root = root
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def root(self) -> Path:
|
|
30
|
+
return self._root
|
|
31
|
+
|
|
32
|
+
def _ensure_root(self) -> Path:
|
|
33
|
+
"""Return the root directory, creating it if it does not exist."""
|
|
34
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
return self._root
|
|
36
|
+
|
|
37
|
+
def notification_dir(self, notification_id: str) -> Path:
|
|
38
|
+
_validate_notification_id(notification_id)
|
|
39
|
+
path = self._ensure_root() / notification_id
|
|
40
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
def notification_path(self, notification_id: str) -> Path:
|
|
44
|
+
_validate_notification_id(notification_id)
|
|
45
|
+
return self.root / notification_id
|
|
46
|
+
|
|
47
|
+
def event_path(self, notification_id: str) -> Path:
|
|
48
|
+
return self.notification_path(notification_id) / self.EVENT_FILE
|
|
49
|
+
|
|
50
|
+
def delivery_path(self, notification_id: str) -> Path:
|
|
51
|
+
return self.notification_path(notification_id) / self.DELIVERY_FILE
|
|
52
|
+
|
|
53
|
+
def create_notification(
|
|
54
|
+
self,
|
|
55
|
+
event: NotificationEvent,
|
|
56
|
+
delivery: NotificationDelivery,
|
|
57
|
+
) -> None:
|
|
58
|
+
notification_dir = self.notification_dir(event.id)
|
|
59
|
+
atomic_json_write(event.model_dump(mode="json"), notification_dir / self.EVENT_FILE)
|
|
60
|
+
atomic_json_write(delivery.model_dump(mode="json"), notification_dir / self.DELIVERY_FILE)
|
|
61
|
+
|
|
62
|
+
def list_notification_ids(self) -> list[str]:
|
|
63
|
+
if not self.root.exists():
|
|
64
|
+
return []
|
|
65
|
+
notification_ids: list[str] = []
|
|
66
|
+
for path in sorted(self.root.iterdir()):
|
|
67
|
+
if not path.is_dir():
|
|
68
|
+
continue
|
|
69
|
+
if not (path / self.EVENT_FILE).exists():
|
|
70
|
+
continue
|
|
71
|
+
notification_ids.append(path.name)
|
|
72
|
+
return notification_ids
|
|
73
|
+
|
|
74
|
+
def read_event(self, notification_id: str) -> NotificationEvent:
|
|
75
|
+
return NotificationEvent.model_validate_json(
|
|
76
|
+
self.event_path(notification_id).read_text(encoding="utf-8")
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def write_event(self, event: NotificationEvent) -> None:
|
|
80
|
+
atomic_json_write(event.model_dump(mode="json"), self.event_path(event.id))
|
|
81
|
+
|
|
82
|
+
def read_delivery(self, notification_id: str) -> NotificationDelivery:
|
|
83
|
+
path = self.delivery_path(notification_id)
|
|
84
|
+
if not path.exists():
|
|
85
|
+
return NotificationDelivery()
|
|
86
|
+
try:
|
|
87
|
+
return NotificationDelivery.model_validate_json(path.read_text(encoding="utf-8"))
|
|
88
|
+
except (OSError, ValidationError, ValueError, UnicodeDecodeError) as exc:
|
|
89
|
+
logger.warning(
|
|
90
|
+
"Failed to read notification delivery {path}; using defaults: {error}",
|
|
91
|
+
path=path,
|
|
92
|
+
error=exc,
|
|
93
|
+
)
|
|
94
|
+
return NotificationDelivery()
|
|
95
|
+
|
|
96
|
+
def write_delivery(self, notification_id: str, delivery: NotificationDelivery) -> None:
|
|
97
|
+
atomic_json_write(delivery.model_dump(mode="json"), self.delivery_path(notification_id))
|
|
98
|
+
|
|
99
|
+
def merged_view(self, notification_id: str) -> NotificationView:
|
|
100
|
+
return NotificationView(
|
|
101
|
+
event=self.read_event(notification_id),
|
|
102
|
+
delivery=self.read_delivery(notification_id),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def list_views(self) -> list[NotificationView]:
|
|
106
|
+
views: list[NotificationView] = []
|
|
107
|
+
for notification_id in self.list_notification_ids():
|
|
108
|
+
try:
|
|
109
|
+
views.append(self.merged_view(notification_id))
|
|
110
|
+
except (OSError, ValidationError, ValueError, UnicodeDecodeError) as exc:
|
|
111
|
+
logger.warning(
|
|
112
|
+
"Skipping invalid notification {notification_id} from {path}: {error}",
|
|
113
|
+
notification_id=notification_id,
|
|
114
|
+
path=self.root / notification_id / self.EVENT_FILE,
|
|
115
|
+
error=exc,
|
|
116
|
+
)
|
|
117
|
+
views.sort(key=lambda view: view.event.created_at, reverse=True)
|
|
118
|
+
return views
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pythinker_code.wire.types import Notification
|
|
4
|
+
|
|
5
|
+
from .models import NotificationView
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_wire_notification(view: NotificationView) -> Notification:
|
|
9
|
+
event = view.event
|
|
10
|
+
return Notification(
|
|
11
|
+
id=event.id,
|
|
12
|
+
category=event.category,
|
|
13
|
+
type=event.type,
|
|
14
|
+
source_kind=event.source_kind,
|
|
15
|
+
source_id=event.source_id,
|
|
16
|
+
title=event.title,
|
|
17
|
+
body=event.body,
|
|
18
|
+
severity=event.severity,
|
|
19
|
+
created_at=event.created_at,
|
|
20
|
+
payload=event.payload,
|
|
21
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Plugin specification parsing and config injection."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PluginError(Exception):
|
|
11
|
+
"""Raised when plugin.json is invalid or an operation fails."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PluginRuntime(BaseModel):
|
|
15
|
+
"""Runtime information written by the host after installation."""
|
|
16
|
+
|
|
17
|
+
host: str
|
|
18
|
+
host_version: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PluginToolSpec(BaseModel):
|
|
22
|
+
"""A tool declared by a plugin."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
description: str
|
|
26
|
+
command: list[str]
|
|
27
|
+
parameters: dict[str, object] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PluginSpec(BaseModel):
|
|
31
|
+
"""Parsed representation of a plugin.json file."""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="ignore")
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
version: str
|
|
37
|
+
description: str = ""
|
|
38
|
+
config_file: str | None = None
|
|
39
|
+
inject: dict[str, str] = Field(default_factory=dict)
|
|
40
|
+
tools: list[PluginToolSpec] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
41
|
+
runtime: PluginRuntime | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
PLUGIN_JSON = "plugin.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_plugin_json(path: Path) -> PluginSpec:
|
|
48
|
+
"""Parse a plugin.json file and return a validated PluginSpec."""
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
52
|
+
raise PluginError(f"Failed to read {path}: {exc}") from exc
|
|
53
|
+
|
|
54
|
+
if "name" not in data:
|
|
55
|
+
raise PluginError(f"Missing required field 'name' in {path}")
|
|
56
|
+
if "version" not in data:
|
|
57
|
+
raise PluginError(f"Missing required field 'version' in {path}")
|
|
58
|
+
if data.get("inject") and not data.get("config_file"):
|
|
59
|
+
raise PluginError(f"'inject' requires 'config_file' in {path}")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
return PluginSpec.model_validate(data)
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
raise PluginError(f"Invalid plugin.json schema in {path}: {exc}") from exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def inject_config(plugin_dir: Path, spec: PluginSpec, values: dict[str, str]) -> None:
|
|
68
|
+
"""Inject host values into the plugin's config file.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
plugin_dir: Root directory of the installed plugin.
|
|
72
|
+
spec: Parsed plugin spec.
|
|
73
|
+
values: Map of standard inject keys to actual values (e.g. {"api_key": "sk-xxx"}).
|
|
74
|
+
"""
|
|
75
|
+
if not spec.inject or not spec.config_file:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
config_path = (plugin_dir / spec.config_file).resolve()
|
|
79
|
+
if not config_path.is_relative_to(plugin_dir.resolve()):
|
|
80
|
+
raise PluginError(f"config_file escapes plugin directory: {spec.config_file}")
|
|
81
|
+
if not config_path.exists():
|
|
82
|
+
raise PluginError(f"Config file not found: {config_path}")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
86
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
87
|
+
raise PluginError(f"Failed to read config file {config_path}: {exc}") from exc
|
|
88
|
+
|
|
89
|
+
for target_path, source_key in spec.inject.items():
|
|
90
|
+
if source_key not in values:
|
|
91
|
+
raise PluginError(f"Host does not provide required inject key '{source_key}'")
|
|
92
|
+
_set_nested(config, target_path, values[source_key])
|
|
93
|
+
|
|
94
|
+
config_path.write_text(
|
|
95
|
+
json.dumps(config, ensure_ascii=False, indent=2),
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def write_runtime(plugin_dir: Path, runtime: PluginRuntime) -> None:
|
|
101
|
+
"""Write runtime info into plugin.json."""
|
|
102
|
+
plugin_json_path = plugin_dir / PLUGIN_JSON
|
|
103
|
+
try:
|
|
104
|
+
data = json.loads(plugin_json_path.read_text(encoding="utf-8"))
|
|
105
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
106
|
+
raise PluginError(f"Failed to read {plugin_json_path}: {exc}") from exc
|
|
107
|
+
data["runtime"] = runtime.model_dump()
|
|
108
|
+
plugin_json_path.write_text(
|
|
109
|
+
json.dumps(data, ensure_ascii=False, indent=2),
|
|
110
|
+
encoding="utf-8",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _set_nested(obj: dict[str, Any], dotted_path: str, value: object) -> None:
|
|
115
|
+
"""Set a value in a nested dict using dot-separated path.
|
|
116
|
+
|
|
117
|
+
Creates intermediate dicts if they don't exist.
|
|
118
|
+
"""
|
|
119
|
+
keys = dotted_path.split(".")
|
|
120
|
+
for key in keys[:-1]:
|
|
121
|
+
if key not in obj or not isinstance(obj[key], dict):
|
|
122
|
+
obj[key] = {}
|
|
123
|
+
obj = obj[key]
|
|
124
|
+
obj[keys[-1]] = value
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Plugin installation, removal, and listing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from pythinker_code.plugin import (
|
|
11
|
+
PLUGIN_JSON,
|
|
12
|
+
PluginError,
|
|
13
|
+
PluginRuntime,
|
|
14
|
+
PluginSpec,
|
|
15
|
+
inject_config,
|
|
16
|
+
parse_plugin_json,
|
|
17
|
+
write_runtime,
|
|
18
|
+
)
|
|
19
|
+
from pythinker_code.share import get_share_dir
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pythinker_code.auth.oauth import OAuthManager
|
|
23
|
+
from pythinker_code.config import Config
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_plugins_dir() -> Path:
|
|
27
|
+
"""Return the plugins installation directory (~/.pythinker/plugins/)."""
|
|
28
|
+
return get_share_dir() / "plugins"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def collect_host_values(config: Config, oauth: OAuthManager) -> dict[str, str]:
|
|
32
|
+
"""Collect host values (api_key, base_url) for plugin injection.
|
|
33
|
+
|
|
34
|
+
Resolves credentials from the default provider, handling OAuth tokens
|
|
35
|
+
and static API keys. Callers that run outside the normal startup flow
|
|
36
|
+
(e.g. ``install_cmd``) should apply environment-variable overrides
|
|
37
|
+
(``augment_provider_with_env_vars``) to the provider **before** calling
|
|
38
|
+
this function; the main app startup already does that.
|
|
39
|
+
"""
|
|
40
|
+
values: dict[str, str] = {}
|
|
41
|
+
if not config.default_model or config.default_model not in config.models:
|
|
42
|
+
return values
|
|
43
|
+
model = config.models[config.default_model]
|
|
44
|
+
if model.provider not in config.providers:
|
|
45
|
+
return values
|
|
46
|
+
provider = config.providers[model.provider]
|
|
47
|
+
api_key = oauth.resolve_api_key(provider.api_key, provider.oauth)
|
|
48
|
+
if api_key:
|
|
49
|
+
values["api_key"] = api_key
|
|
50
|
+
values["base_url"] = provider.base_url
|
|
51
|
+
return values
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validate_name(name: str, plugins_dir: Path) -> Path:
|
|
55
|
+
"""Resolve and validate plugin name, returning the safe destination path."""
|
|
56
|
+
dest = (plugins_dir / name).resolve()
|
|
57
|
+
if not dest.is_relative_to(plugins_dir.resolve()):
|
|
58
|
+
raise PluginError(f"Invalid plugin name: {name}")
|
|
59
|
+
return dest
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def install_plugin(
|
|
63
|
+
*,
|
|
64
|
+
source: Path,
|
|
65
|
+
plugins_dir: Path,
|
|
66
|
+
host_values: dict[str, str],
|
|
67
|
+
host_name: str,
|
|
68
|
+
host_version: str,
|
|
69
|
+
) -> PluginSpec:
|
|
70
|
+
"""Install a plugin from a source directory.
|
|
71
|
+
|
|
72
|
+
Stages the new copy to a temp dir first, so a failed upgrade
|
|
73
|
+
does not destroy the previous installation.
|
|
74
|
+
"""
|
|
75
|
+
source_plugin_json = source / PLUGIN_JSON
|
|
76
|
+
if not source_plugin_json.exists():
|
|
77
|
+
raise PluginError(f"No plugin.json found in {source}")
|
|
78
|
+
|
|
79
|
+
spec = parse_plugin_json(source_plugin_json)
|
|
80
|
+
dest = _validate_name(spec.name, plugins_dir)
|
|
81
|
+
|
|
82
|
+
# Stage to a temp dir inside plugins_dir so rename is atomic on same fs
|
|
83
|
+
plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
staging = Path(tempfile.mkdtemp(prefix=f".{spec.name}-", dir=plugins_dir))
|
|
85
|
+
try:
|
|
86
|
+
# Copy source into staging
|
|
87
|
+
staging_plugin = staging / spec.name
|
|
88
|
+
shutil.copytree(source, staging_plugin)
|
|
89
|
+
|
|
90
|
+
# Apply inject + runtime on the staged copy
|
|
91
|
+
inject_config(staging_plugin, spec, host_values)
|
|
92
|
+
runtime = PluginRuntime(host=host_name, host_version=host_version)
|
|
93
|
+
write_runtime(staging_plugin, runtime)
|
|
94
|
+
|
|
95
|
+
# Swap: remove old, move staged into place
|
|
96
|
+
if dest.exists():
|
|
97
|
+
shutil.rmtree(dest)
|
|
98
|
+
staging_plugin.rename(dest)
|
|
99
|
+
except Exception:
|
|
100
|
+
# On any failure, clean up staging but leave existing install intact
|
|
101
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
102
|
+
raise
|
|
103
|
+
finally:
|
|
104
|
+
# Clean up staging dir shell (may be empty after successful rename)
|
|
105
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
106
|
+
|
|
107
|
+
# Re-read to return the installed spec (with runtime)
|
|
108
|
+
return parse_plugin_json(dest / PLUGIN_JSON)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def refresh_plugin_configs(plugins_dir: Path, host_values: dict[str, str]) -> None:
|
|
112
|
+
"""Re-inject host values into all installed plugin config files.
|
|
113
|
+
|
|
114
|
+
Called at startup so that OAuth tokens and other credentials
|
|
115
|
+
stay fresh even after the initial install.
|
|
116
|
+
"""
|
|
117
|
+
if not plugins_dir.is_dir():
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
for child in sorted(plugins_dir.iterdir()):
|
|
121
|
+
plugin_json = child / PLUGIN_JSON
|
|
122
|
+
if not child.is_dir() or not plugin_json.is_file():
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
spec = parse_plugin_json(plugin_json)
|
|
126
|
+
if spec.inject and spec.config_file:
|
|
127
|
+
inject_config(child, spec, host_values)
|
|
128
|
+
except Exception:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def list_plugins(plugins_dir: Path) -> list[PluginSpec]:
|
|
133
|
+
"""List all installed plugins."""
|
|
134
|
+
if not plugins_dir.is_dir():
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
plugins: list[PluginSpec] = []
|
|
138
|
+
for child in sorted(plugins_dir.iterdir()):
|
|
139
|
+
plugin_json = child / PLUGIN_JSON
|
|
140
|
+
if child.is_dir() and plugin_json.is_file():
|
|
141
|
+
try:
|
|
142
|
+
plugins.append(parse_plugin_json(plugin_json))
|
|
143
|
+
except PluginError:
|
|
144
|
+
continue
|
|
145
|
+
return plugins
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def remove_plugin(name: str, plugins_dir: Path) -> None:
|
|
149
|
+
"""Remove an installed plugin."""
|
|
150
|
+
dest = _validate_name(name, plugins_dir)
|
|
151
|
+
if not dest.exists():
|
|
152
|
+
raise PluginError(f"Plugin '{name}' not found in {plugins_dir}")
|
|
153
|
+
shutil.rmtree(dest)
|