pythinker-code 0.45.0__py3-none-any.whl → 0.47.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 +98 -0
- pythinker_code/acp/session.py +8 -0
- pythinker_code/agents/default/agent.yaml +4 -0
- pythinker_code/app.py +19 -2
- pythinker_code/cli/__init__.py +13 -2
- pythinker_code/cli/mcp.py +104 -27
- pythinker_code/cli/plugin.py +230 -1
- pythinker_code/config.py +74 -0
- pythinker_code/llm.py +8 -2
- pythinker_code/plugin/artifacts.py +103 -0
- pythinker_code/plugin/dependency.py +91 -0
- pythinker_code/plugin/directories.py +94 -0
- pythinker_code/plugin/install.py +417 -0
- pythinker_code/plugin/installed.py +128 -0
- pythinker_code/plugin/integration.py +251 -0
- pythinker_code/plugin/loader.py +178 -0
- pythinker_code/plugin/manifest.py +260 -0
- pythinker_code/plugin/marketplace.py +208 -0
- pythinker_code/plugin/options.py +45 -0
- pythinker_code/plugin/policy.py +87 -0
- pythinker_code/prompt_templates.py +5 -1
- pythinker_code/prompts/__init__.py +5 -0
- pythinker_code/skill/__init__.py +6 -8
- pythinker_code/soul/agent.py +42 -17
- pythinker_code/soul/compaction.py +39 -0
- pythinker_code/soul/dynamic_injections/agent_list.py +85 -0
- pythinker_code/soul/dynamic_injections/git_status.py +63 -0
- pythinker_code/soul/live_tokens.py +47 -0
- pythinker_code/soul/pythinkersoul.py +179 -4
- pythinker_code/soul/toolset.py +421 -88
- pythinker_code/subagents/discovery.py +32 -1
- pythinker_code/telemetry/names.py +44 -0
- pythinker_code/tools/agent/__init__.py +63 -1
- pythinker_code/tools/agent/description.md +9 -0
- pythinker_code/tools/file/read_media.py +21 -4
- pythinker_code/tools/mcp_resource/__init__.py +78 -1
- pythinker_code/tools/mcp_resource/prompt_description.md +17 -0
- pythinker_code/tools/plan/__init__.py +15 -1
- pythinker_code/tools/plan/description.md +3 -0
- pythinker_code/tools/recall/__init__.py +71 -10
- pythinker_code/tools/recall/description.md +6 -3
- pythinker_code/tools/todo/__init__.py +20 -0
- pythinker_code/tools/tool_search/__init__.py +74 -0
- pythinker_code/tools/tool_search/tool_search.md +7 -0
- pythinker_code/tools/worktree/__init__.py +218 -0
- pythinker_code/tools/worktree/enter_worktree.md +9 -0
- pythinker_code/tools/worktree/exit_worktree.md +5 -0
- pythinker_code/ui/shell/__init__.py +58 -4
- pythinker_code/ui/shell/components/markdown.py +41 -1
- pythinker_code/ui/shell/components/report.py +13 -3
- pythinker_code/ui/shell/prompt.py +69 -10
- pythinker_code/ui/shell/slash.py +59 -1
- pythinker_code/ui/shell/tool_renderers/agent.py +69 -5
- pythinker_code/ui/shell/update.py +7 -1
- pythinker_code/ui/shell/visualize/_blocks.py +14 -3
- pythinker_code/ui/shell/visualize/_interactive.py +10 -2
- pythinker_code/ui/shell/visualize/_live_view.py +21 -5
- pythinker_code/ui/theme.py +2 -2
- pythinker_code/utils/io.py +31 -0
- pythinker_code/utils/mcp_names.py +60 -0
- pythinker_code/utils/media_limits.py +20 -0
- pythinker_code/web/static/assets/architecture-7EHR7CIX-BgGrIJTG.js +1 -0
- pythinker_code/web/static/assets/{architectureDiagram-3BPJPVTR-Cmrv4t4A.js → architectureDiagram-3BPJPVTR-Czbbrggh.js} +1 -1
- pythinker_code/web/static/assets/{blockDiagram-GPEHLZMM-DJtt-l4b.js → blockDiagram-GPEHLZMM-DuGxQ4gb.js} +1 -1
- pythinker_code/web/static/assets/{bootstrap-GQDZdCZn.js → bootstrap-BRW86uxF.js} +5 -5
- pythinker_code/web/static/assets/{c4Diagram-AAUBKEIU-o0KsSeIT.js → c4Diagram-AAUBKEIU-C9QqOozv.js} +1 -1
- pythinker_code/web/static/assets/channel-Bw8F6co7.js +1 -0
- pythinker_code/web/static/assets/{chunk-2J33WTMH-CVPDc3uI.js → chunk-2J33WTMH-CLDwXFrS.js} +1 -1
- pythinker_code/web/static/assets/{chunk-3OPIFGDE-Tu9TJ8FO.js → chunk-3OPIFGDE-CgPHKDsU.js} +1 -1
- pythinker_code/web/static/assets/{chunk-5ZQYHXKU-CoBmhCur.js → chunk-5ZQYHXKU-DhesutHE.js} +1 -1
- pythinker_code/web/static/assets/{chunk-727SXJPM-DByzCodm.js → chunk-727SXJPM-mTEiNOi3.js} +1 -1
- pythinker_code/web/static/assets/{chunk-AQP2D5EJ-DclqBNWp.js → chunk-AQP2D5EJ-DlSrZz-k.js} +1 -1
- pythinker_code/web/static/assets/{chunk-CSCIHK7Q-CUIX1-rb.js → chunk-CSCIHK7Q-D0YCOqn9.js} +1 -1
- pythinker_code/web/static/assets/{chunk-JAPRZBRM-gOR8tSRc.js → chunk-JAPRZBRM-CLY2Nkh7.js} +4 -4
- pythinker_code/web/static/assets/{chunk-KSCS5N6A-hta5hym2.js → chunk-KSCS5N6A-DbcFg-5f.js} +1 -1
- pythinker_code/web/static/assets/{chunk-L5ZTLDWV-cj__FNrg.js → chunk-L5ZTLDWV-8WkWOyc_.js} +1 -1
- pythinker_code/web/static/assets/{chunk-LZXEDZCA-CSHDE7c1.js → chunk-LZXEDZCA-BUgkoo7u.js} +2 -2
- pythinker_code/web/static/assets/{chunk-ND2GUHAM-DMFQO6Cb.js → chunk-ND2GUHAM-C0HJXIEZ.js} +1 -1
- pythinker_code/web/static/assets/{chunk-NZK2D7GU-Cjm014we.js → chunk-NZK2D7GU-DBQjspgy.js} +1 -1
- pythinker_code/web/static/assets/{chunk-O5CBEL6O-BSr1l1Fd.js → chunk-O5CBEL6O-Y2JuT_28.js} +1 -1
- pythinker_code/web/static/assets/{chunk-WU5MYG2G-veNzkohb.js → chunk-WU5MYG2G-BSQmvLlf.js} +1 -1
- pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-B3WrcDb4.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-B3WrcDb4.js +1 -0
- pythinker_code/web/static/assets/{code-block-IT6T5CEO-DTgIHAkQ.js → code-block-IT6T5CEO-COJFtT1s.js} +1 -1
- pythinker_code/web/static/assets/{dagre-BM42HDAG-D-_HCbAC.js → dagre-BM42HDAG-C-63xbhz.js} +1 -1
- pythinker_code/web/static/assets/{diagram-2AECGRRQ-CvX58Rpg.js → diagram-2AECGRRQ-BA3t5dvt.js} +1 -1
- pythinker_code/web/static/assets/{diagram-5GNKFQAL-CsRaiNkg.js → diagram-5GNKFQAL-Cn_tPFPy.js} +1 -1
- pythinker_code/web/static/assets/{diagram-KO2AKTUF-Dn6aiXG5.js → diagram-KO2AKTUF-CNLlhwgh.js} +1 -1
- pythinker_code/web/static/assets/{diagram-LMA3HP47-DXQlab3r.js → diagram-LMA3HP47-BubQEJye.js} +1 -1
- pythinker_code/web/static/assets/{diagram-OG6HWLK6-BWB_FmTS.js → diagram-OG6HWLK6-B1zoPW4f.js} +1 -1
- pythinker_code/web/static/assets/{dist-C8Q1EY4S.js → dist-C-YVtz9k.js} +1 -1
- pythinker_code/web/static/assets/{erDiagram-TEJ5UH35-BrQiudEc.js → erDiagram-TEJ5UH35-BrFwtQIS.js} +1 -1
- pythinker_code/web/static/assets/eventmodeling-FCH6USID-DjypEbTa.js +1 -0
- pythinker_code/web/static/assets/{flowDiagram-I6XJVG4X-C6jARfsw.js → flowDiagram-I6XJVG4X-CMde8qln.js} +1 -1
- pythinker_code/web/static/assets/{ganttDiagram-6RSMTGT7-DH-i7Wci.js → ganttDiagram-6RSMTGT7-DqKHO3Gj.js} +1 -1
- pythinker_code/web/static/assets/{gitGraph-WXDBUCRP-lU8PeAwh.js → gitGraph-WXDBUCRP-DSsm7EeH.js} +1 -1
- pythinker_code/web/static/assets/{gitGraphDiagram-PVQCEYII-JOoIM40T.js → gitGraphDiagram-PVQCEYII-BPXFnSJu.js} +1 -1
- pythinker_code/web/static/assets/{index-BJIE96ev.js → index-DrLJ6Yzo.js} +2 -2
- pythinker_code/web/static/assets/{info-J43DQDTF-B9a8E9HF.js → info-J43DQDTF-Ckpo8amK.js} +1 -1
- pythinker_code/web/static/assets/{infoDiagram-5YYISTIA-D5wmE10q.js → infoDiagram-5YYISTIA-BvhEvq2y.js} +1 -1
- pythinker_code/web/static/assets/{ishikawaDiagram-YF4QCWOH-DzNdTJqt.js → ishikawaDiagram-YF4QCWOH-BGVm0gpH.js} +1 -1
- pythinker_code/web/static/assets/{journeyDiagram-JHISSGLW-CwevEltw.js → journeyDiagram-JHISSGLW-DGjKVrAu.js} +1 -1
- pythinker_code/web/static/assets/{kanban-definition-UN3LZRKU-BAmLyJgC.js → kanban-definition-UN3LZRKU-DF4Ru5vp.js} +1 -1
- pythinker_code/web/static/assets/{line-CktbshjS.js → line-zsX5pPNl.js} +1 -1
- pythinker_code/web/static/assets/mermaid-VLURNSYL-BY1nFAIo.js +1 -0
- pythinker_code/web/static/assets/{mermaid-parser.core-B1Hc3VHx.js → mermaid-parser.core-DgXtwV12.js} +2 -2
- pythinker_code/web/static/assets/{mermaid.core-zqRnwN3B.js → mermaid.core-DDTnZJwj.js} +3 -3
- pythinker_code/web/static/assets/{mindmap-definition-RKZ34NQL-CamoLUlD.js → mindmap-definition-RKZ34NQL-DP-UXBwG.js} +1 -1
- pythinker_code/web/static/assets/{packet-YPE3B663-s78KeeiS.js → packet-YPE3B663-Dx3OSICL.js} +1 -1
- pythinker_code/web/static/assets/{pie-LRSECV5Y-DSusaq_V.js → pie-LRSECV5Y-IY3KukRR.js} +1 -1
- pythinker_code/web/static/assets/{pieDiagram-4H26LBE5-BcJSWsmz.js → pieDiagram-4H26LBE5-Ch0k4QrE.js} +1 -1
- pythinker_code/web/static/assets/{quadrantDiagram-W4KKPZXB-Cxp3MYGY.js → quadrantDiagram-W4KKPZXB-B8RbtJ2P.js} +1 -1
- pythinker_code/web/static/assets/{radar-GUYGQ44K-sZyAfli_.js → radar-GUYGQ44K-D0YLOyN_.js} +1 -1
- pythinker_code/web/static/assets/{requirementDiagram-4Y6WPE33-DSaeIAHw.js → requirementDiagram-4Y6WPE33-BTYkIO2O.js} +1 -1
- pythinker_code/web/static/assets/{sankeyDiagram-5OEKKPKP-BI87xNZ8.js → sankeyDiagram-5OEKKPKP-C3lqfCj1.js} +1 -1
- pythinker_code/web/static/assets/{sequenceDiagram-3UESZ5HK-B3mOEaPg.js → sequenceDiagram-3UESZ5HK-CxovAw-L.js} +1 -1
- pythinker_code/web/static/assets/{stateDiagram-AJRCARHV-CloKSCZq.js → stateDiagram-AJRCARHV-BAkLX0tZ.js} +1 -1
- pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-DsW61nNZ.js +1 -0
- pythinker_code/web/static/assets/{timeline-definition-PNZ67QCA-dy1xxojp.js → timeline-definition-PNZ67QCA-D8o-ffC-.js} +1 -1
- pythinker_code/web/static/assets/{treeView-BLDUP644-CouMM8Ld.js → treeView-BLDUP644-CKwJMtWj.js} +1 -1
- pythinker_code/web/static/assets/{treemap-LRROVOQU-BurOGxmB.js → treemap-LRROVOQU-0nn1YylS.js} +1 -1
- pythinker_code/web/static/assets/{vennDiagram-CIIHVFJN-ceoQfI8-.js → vennDiagram-CIIHVFJN-BEkXoyJ2.js} +1 -1
- pythinker_code/web/static/assets/{wardley-L42UT6IY-DQIeshOY.js → wardley-L42UT6IY-BielzF7q.js} +1 -1
- pythinker_code/web/static/assets/{wardleyDiagram-YWT4CUSO-yJgnjbxz.js → wardleyDiagram-YWT4CUSO-CbsH-9xk.js} +1 -1
- pythinker_code/web/static/assets/{xychartDiagram-2RQKCTM6-jMkhXWCc.js → xychartDiagram-2RQKCTM6-D4gCiMTY.js} +1 -1
- pythinker_code/web/static/index.html +1 -1
- pythinker_code/wire/server.py +24 -1
- pythinker_code/wire/types.py +81 -1
- {pythinker_code-0.45.0.dist-info → pythinker_code-0.47.0.dist-info}/METADATA +23 -24
- {pythinker_code-0.45.0.dist-info → pythinker_code-0.47.0.dist-info}/RECORD +134 -111
- pythinker_code/web/static/assets/architecture-7EHR7CIX-CxeoRZcL.js +0 -1
- pythinker_code/web/static/assets/channel-CzOfc6qg.js +0 -1
- pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-Cl__vhhZ.js +0 -1
- pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-Cl__vhhZ.js +0 -1
- pythinker_code/web/static/assets/eventmodeling-FCH6USID-1z7xo-Al.js +0 -1
- pythinker_code/web/static/assets/mermaid-VLURNSYL-B9qVFSXg.js +0 -1
- pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-bwtGCtLW.js +0 -1
- {pythinker_code-0.45.0.dist-info → pythinker_code-0.47.0.dist-info}/WHEEL +0 -0
- {pythinker_code-0.45.0.dist-info → pythinker_code-0.47.0.dist-info}/entry_points.txt +0 -0
- {pythinker_code-0.45.0.dist-info → pythinker_code-0.47.0.dist-info}/licenses/LICENSE +0 -0
- {pythinker_code-0.45.0.dist-info → pythinker_code-0.47.0.dist-info}/licenses/NOTICE +0 -0
pythinker_code/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,104 @@ GitHub Releases page; `0.8.0` is the new starting line.
|
|
|
15
15
|
|
|
16
16
|
## Unreleased
|
|
17
17
|
|
|
18
|
+
## 0.47.0 (2026-06-16)
|
|
19
|
+
|
|
20
|
+
- **Plugin marketplaces and activation policy.** `pythinker plugin marketplace` can add,
|
|
21
|
+
refresh, install, and uninstall Claude/Codex-compatible marketplace plugins. Plugins
|
|
22
|
+
installed for Claude Code or Codex are auto-detected (no symlink): their safe artifacts
|
|
23
|
+
(skills, commands, agents) activate by default, while executable artifacts (hooks, MCP
|
|
24
|
+
servers) stay opt-in. Config `plugins.discover_external`, `plugins.external_exec`,
|
|
25
|
+
`plugins.enabled`, and `plugins.disabled` — plus `pythinker plugin enable/disable <name>`
|
|
26
|
+
— control which installed plugins contribute artifacts. Hook and MCP commands
|
|
27
|
+
expand `${CLAUDE_PLUGIN_ROOT}`/`${PYTHINKER_PLUGIN_ROOT}` and
|
|
28
|
+
`${CLAUDE_PLUGIN_DATA}`/`${PYTHINKER_PLUGIN_DATA}`.
|
|
29
|
+
- **Plugin dependencies.** Plugins may declare `dependencies`; installing one pulls its
|
|
30
|
+
transitive dependencies from the same marketplace (cross-marketplace deps are blocked),
|
|
31
|
+
and a plugin whose dependencies aren't present+enabled is disabled at load instead of
|
|
32
|
+
half-activating.
|
|
33
|
+
- **Plugin options (`userConfig`).** `${user_config.KEY}` in a plugin's MCP server configs
|
|
34
|
+
and hook commands is filled from `[plugins.options.<plugin>]` config; an artifact that
|
|
35
|
+
references an unconfigured option is skipped rather than run blank. (Content substitution,
|
|
36
|
+
`PYTHINKER_PLUGIN_OPTION_*` hook env vars, and keychain-backed sensitive storage are not
|
|
37
|
+
yet implemented.)
|
|
38
|
+
- **MCP tool lists refresh automatically when servers change.** Connected MCP
|
|
39
|
+
sessions stay open for `tools/list_changed` (and resources/prompts) notifications;
|
|
40
|
+
inventory is re-published without a manual `/mcp refresh`.
|
|
41
|
+
- **Shell live token readouts track output throughput.** The spinner and background
|
|
42
|
+
status line show session-wide output tokens produced during the current turn or
|
|
43
|
+
background stretch instead of the context-size snapshot.
|
|
44
|
+
- **MCP servers can be managed without a full reload.** `/mcp disconnect`, `/mcp reconnect`, and
|
|
45
|
+
`/mcp refresh` (or `retry`) update the live toolset for one server; disconnect unregisters its
|
|
46
|
+
tools and marks the server failed until reconnect.
|
|
47
|
+
- **Recall search matches session ids and plan slugs.** Prior-session keyword search now indexes
|
|
48
|
+
`session_id` and `plan_slug` in addition to titles so agents can find plan-linked sessions by slug.
|
|
49
|
+
- **Wire and ACP surfaces now get max-steps handoff summaries.** When a turn hits the step ceiling,
|
|
50
|
+
wire clients receive a streamed handoff event plus a `handoff` field on the `max_steps_reached`
|
|
51
|
+
result; ACP sessions emit the same summary text before returning `max_turn_requests`.
|
|
52
|
+
- **MCP CLI commands resolve normalized server names.** `mcp remove`, `mcp auth`, `mcp test`, and
|
|
53
|
+
`reset-auth` accept display names with spaces or slashes and map them to stored config keys; config
|
|
54
|
+
load applies the same normalization as add.
|
|
55
|
+
- **Compaction failure circuit breaker respects thresholds above one.** A proactive compaction
|
|
56
|
+
failure below `max_compaction_failures` no longer aborts the turn; the handoff fires only after
|
|
57
|
+
the configured number of consecutive failures.
|
|
58
|
+
- **AI eval gate schema is self-contained under `tests_ai/`.** Shared eval-case types live in
|
|
59
|
+
`tests_ai/eval_schema.py` so isolated `tests_ai` runs do not import from `tests_e2e`.
|
|
60
|
+
- **Softer TUI chrome in the dark theme.** Panel borders (welcome banner, menus) and the input-area
|
|
61
|
+
rules now render in a mid grey (`#8a8d91`) instead of near-white, for a less glaring look.
|
|
62
|
+
- **Stop-time memory extraction can now be enabled explicitly.** Added an opt-in
|
|
63
|
+
`memory.harvest_on_stop` setting that stages safe assistant decisions, blockers, evidence, and
|
|
64
|
+
next steps into the existing scratchpad recall flow at turn end without writing directly to
|
|
65
|
+
durable `MEMORY.md`.
|
|
66
|
+
- **Agents can now discover visible tools and temporarily work from a session worktree.** Added
|
|
67
|
+
`ToolSearch` plus root-session `EnterWorktree` and `ExitWorktree` tools so agents can find
|
|
68
|
+
currently available capabilities by keyword and isolate a session's operational working directory
|
|
69
|
+
in a git worktree without deleting user work on exit.
|
|
70
|
+
- **Root sessions now get a bounded git snapshot in the prompt.** When `git_status_injection` is
|
|
71
|
+
enabled (default), the agent receives branch, dirty-file summary, and recent commits as an
|
|
72
|
+
explicitly stale point-in-time reminder; disable via config or set `git_status_injection = false`.
|
|
73
|
+
- **Context compaction and MCP tool registration now fail more predictably.** Proactive compaction
|
|
74
|
+
failures hand back with an explicit `compaction_failed` stop instead of bubbling an unstructured
|
|
75
|
+
loop error, and MCP duplicate tool-name resolution now follows configured server order instead of
|
|
76
|
+
connection completion order. Tool hooks also retain the original model input even if a tool
|
|
77
|
+
mutates a nested argument object during execution.
|
|
78
|
+
- **Recall can now read bounded transcript windows.** `Recall(mode="read")` accepts
|
|
79
|
+
`message_offset` and `max_messages` so agents can inspect a precise, sanitized slice of a prior
|
|
80
|
+
workspace session without pulling the whole transcript into context.
|
|
81
|
+
- **MCP prompt templates can now be invoked from connected servers.** `InvokeMcpPrompt` renders a
|
|
82
|
+
server-published prompt with structured arguments and wraps the returned messages as untrusted
|
|
83
|
+
input for the model.
|
|
84
|
+
- **Telemetry, MCP config, and shell UX hardening.** Tool spans and metrics sanitize MCP/plugin
|
|
85
|
+
names; `mcp.json` load paths inject docker `--rm` and normalize server keys with collision
|
|
86
|
+
errors; shell suggestions accept via Alt+S into the prompt; markdown agent frontmatter maps
|
|
87
|
+
`max_turns`/`disallowed_tools`; `ReadMediaFile` enforces per-kind byte/pixel caps; written plans
|
|
88
|
+
without a Verification section get a soft warning; AI eval budgets can gate `tests_ai` reports.
|
|
89
|
+
- **Plan-mode exit guidance now requires verification.** The `ExitPlanMode` tool now tells agents
|
|
90
|
+
that written plans must include a Verification section with the smallest command, test, or check
|
|
91
|
+
for each meaningful change.
|
|
92
|
+
- **Agent-loop observability now emits explicit Wire events for key runtime state.** Added
|
|
93
|
+
`TodoListUpdated`, `SubagentToolFallback`, `AgentListDelta`, `ToolUseSkipped`, and
|
|
94
|
+
`ContextOverflowRecovered` events, with todo updates, subagent launch fallbacks, same-step tool
|
|
95
|
+
reuse/policy skips for tools that explicitly opt in, agent-list injections, and
|
|
96
|
+
context-overflow recovery now surfaced best-effort over Wire without changing existing tool
|
|
97
|
+
results.
|
|
98
|
+
- **Spend ceilings now warn before they stop a session.** When `max_session_cost_usd` is configured,
|
|
99
|
+
the loop appends a bounded system reminder after a turn crosses `budget_nudge_ratio` of the
|
|
100
|
+
ceiling, nudging the agent to conserve budget without auto-continuing or hiding a later
|
|
101
|
+
`budget_exhausted` stop.
|
|
102
|
+
- **The Agent tool description now gives clearer prompt-briefing guidance.** Fresh subagents should
|
|
103
|
+
receive the goal, scope, expected output contract, and verification criteria; the Haiku-style
|
|
104
|
+
tool-use summary from the blackbox reference was deliberately not ported.
|
|
105
|
+
|
|
106
|
+
Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.47.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
|
|
107
|
+
|
|
108
|
+
## 0.46.0 (2026-06-14)
|
|
109
|
+
|
|
110
|
+
- **Startup auto-update now picks up new releases within half an hour instead of up to a day.** The background update check was throttled to once every 24h, so a freshly published release could go unnoticed for a full day after the last check; the interval is now 30 minutes. The silent installer also no longer marks the throttle *before* the network call — a transient startup network error returns `FAILED` and is retried on the next launch instead of suppressing updates for the whole window.
|
|
111
|
+
- **The welcome banner now keeps the robot mark visible in compact terminals.** Below ~68 columns the robot was dropped (it could not sit beside the welcome copy); it now stacks centered above the copy instead, so the mark stays on screen at any width that can render its Unicode glyphs. ASCII-only terminals are unaffected.
|
|
112
|
+
- **A persistent update notice now sits directly under the prompt input.** When a newer release is available the footer shows a yellow `↑ Update available — vX · /update` line; once a release has been installed in the background it switches to a restart-to-apply message instead of pointing at `/update`, and it clears after you restart onto the new version. The line is suppressed for dismissed versions, disabled auto-update, and source checkouts.
|
|
113
|
+
|
|
114
|
+
Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.46.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
|
|
115
|
+
|
|
18
116
|
## 0.45.0 (2026-06-14)
|
|
19
117
|
|
|
20
118
|
- **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`).
|
pythinker_code/acp/session.py
CHANGED
|
@@ -17,6 +17,7 @@ from pythinker_code.acp.convert import (
|
|
|
17
17
|
from pythinker_code.acp.types import ACPContentBlock
|
|
18
18
|
from pythinker_code.app import PythinkerCLI
|
|
19
19
|
from pythinker_code.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled
|
|
20
|
+
from pythinker_code.soul.btw import generate_max_steps_handoff
|
|
20
21
|
from pythinker_code.tools import extract_key_argument
|
|
21
22
|
from pythinker_code.utils.logging import logger
|
|
22
23
|
from pythinker_code.wire.types import (
|
|
@@ -242,6 +243,13 @@ class ACPSession:
|
|
|
242
243
|
raise acp.RequestError.internal_error({"error": str(e)}) from e
|
|
243
244
|
except MaxStepsReached as e:
|
|
244
245
|
logger.warning("Max steps reached: {n_steps}", n_steps=e.n_steps)
|
|
246
|
+
try:
|
|
247
|
+
handoff = await generate_max_steps_handoff(self._cli.soul)
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.warning("Max-steps handoff failed", exc_info=True)
|
|
250
|
+
handoff = None
|
|
251
|
+
if handoff:
|
|
252
|
+
await self._send_text(f"\n── handoff ──\n{handoff}")
|
|
245
253
|
return acp.PromptResponse(stop_reason="max_turn_requests")
|
|
246
254
|
except RunCancelled:
|
|
247
255
|
logger.info("Prompt cancelled by user")
|
|
@@ -12,6 +12,9 @@ agent:
|
|
|
12
12
|
# - "pythinker_code.tools.think:Think"
|
|
13
13
|
- "pythinker_code.tools.ask_user:AskUserQuestion"
|
|
14
14
|
- "pythinker_code.tools.todo:SetTodoList"
|
|
15
|
+
- "pythinker_code.tools.tool_search:ToolSearch"
|
|
16
|
+
- "pythinker_code.tools.worktree:EnterWorktree"
|
|
17
|
+
- "pythinker_code.tools.worktree:ExitWorktree"
|
|
15
18
|
- "pythinker_code.tools.goal:UpdateGoal"
|
|
16
19
|
- "pythinker_code.tools.progress:Progress"
|
|
17
20
|
- "pythinker_code.tools.suggest:Suggest"
|
|
@@ -35,6 +38,7 @@ agent:
|
|
|
35
38
|
- "pythinker_code.tools.web:FetchURL"
|
|
36
39
|
- "pythinker_code.tools.mcp_resource:ListMcpResources"
|
|
37
40
|
- "pythinker_code.tools.mcp_resource:ReadMcpResource"
|
|
41
|
+
- "pythinker_code.tools.mcp_resource:InvokeMcpPrompt"
|
|
38
42
|
- "pythinker_code.tools.plan:ExitPlanMode"
|
|
39
43
|
- "pythinker_code.tools.plan.enter:EnterPlanMode"
|
|
40
44
|
subagents:
|
pythinker_code/app.py
CHANGED
|
@@ -254,6 +254,21 @@ class PythinkerCLI:
|
|
|
254
254
|
config.loop_control.max_ralph_iterations = max_ralph_iterations
|
|
255
255
|
logger.info("Loaded config: {config}", config=config)
|
|
256
256
|
|
|
257
|
+
# Install the plugin activation policy for this session so artifact
|
|
258
|
+
# collectors (skills/agents/commands/hooks/mcp) honor config-configured
|
|
259
|
+
# enable-state and the external (Claude/Codex) opt-in.
|
|
260
|
+
from pythinker_code.plugin.policy import policy_from_config, set_plugin_policy
|
|
261
|
+
|
|
262
|
+
set_plugin_policy(
|
|
263
|
+
policy_from_config(
|
|
264
|
+
config.plugins.discover_external,
|
|
265
|
+
config.plugins.external_exec,
|
|
266
|
+
config.plugins.enabled,
|
|
267
|
+
config.plugins.disabled,
|
|
268
|
+
config.plugins.options,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
257
272
|
_phase_t = time.monotonic()
|
|
258
273
|
oauth = OAuthManager(config)
|
|
259
274
|
|
|
@@ -392,10 +407,12 @@ class PythinkerCLI:
|
|
|
392
407
|
# Already in plan mode from restored session, trigger activation reminder
|
|
393
408
|
soul.schedule_plan_activation_reminder()
|
|
394
409
|
|
|
395
|
-
# Create and inject hook engine
|
|
410
|
+
# Create and inject hook engine. Enabled plugins contribute lifecycle
|
|
411
|
+
# hooks; config hooks come first so a project hook is never shadowed.
|
|
396
412
|
from pythinker_code.hooks.engine import HookEngine
|
|
413
|
+
from pythinker_code.plugin.integration import plugin_hook_defs
|
|
397
414
|
|
|
398
|
-
hook_engine = HookEngine(config.hooks, cwd=str(session.work_dir))
|
|
415
|
+
hook_engine = HookEngine([*config.hooks, *plugin_hook_defs()], cwd=str(session.work_dir))
|
|
399
416
|
if config.disabled_project_hooks:
|
|
400
417
|
# The load-time logger.warning only reaches shell users; publish a
|
|
401
418
|
# notification so web/ACP frontends also learn why their project
|
pythinker_code/cli/__init__.py
CHANGED
|
@@ -270,9 +270,13 @@ def _load_mcp_configs_from_cli_inputs(
|
|
|
270
270
|
file_configs.append(project_mcp_file)
|
|
271
271
|
|
|
272
272
|
configs: list[Any] = []
|
|
273
|
+
from pythinker_code.exception import MCPConfigError
|
|
274
|
+
|
|
275
|
+
from .mcp import prepare_mcp_config_dict
|
|
276
|
+
|
|
273
277
|
for conf in file_configs:
|
|
274
278
|
try:
|
|
275
|
-
configs.append(json.loads(conf.read_text(encoding="utf-8")))
|
|
279
|
+
configs.append(prepare_mcp_config_dict(json.loads(conf.read_text(encoding="utf-8"))))
|
|
276
280
|
except json.JSONDecodeError as e:
|
|
277
281
|
raise typer.BadParameter(
|
|
278
282
|
f"Invalid JSON in MCP config file {conf}: {e}",
|
|
@@ -283,12 +287,19 @@ def _load_mcp_configs_from_cli_inputs(
|
|
|
283
287
|
f"Cannot read MCP config file {conf}: {e}",
|
|
284
288
|
param_hint="--mcp-config-file",
|
|
285
289
|
) from e
|
|
290
|
+
except MCPConfigError as e:
|
|
291
|
+
raise typer.BadParameter(
|
|
292
|
+
f"Invalid MCP config in file {conf}: {e}",
|
|
293
|
+
param_hint="--mcp-config-file",
|
|
294
|
+
) from e
|
|
286
295
|
|
|
287
296
|
for conf in raw_mcp_config:
|
|
288
297
|
try:
|
|
289
|
-
configs.append(json.loads(conf))
|
|
298
|
+
configs.append(prepare_mcp_config_dict(json.loads(conf)))
|
|
290
299
|
except json.JSONDecodeError as e:
|
|
291
300
|
raise typer.BadParameter(f"Invalid JSON: {e}", param_hint="--mcp-config") from e
|
|
301
|
+
except MCPConfigError as e:
|
|
302
|
+
raise typer.BadParameter(f"Invalid MCP config: {e}", param_hint="--mcp-config") from e
|
|
292
303
|
|
|
293
304
|
for path in _yaml_files_with_misplaced_mcp_servers():
|
|
294
305
|
from pythinker_code.utils.logging import logger
|
pythinker_code/cli/mcp.py
CHANGED
|
@@ -2,7 +2,7 @@ import contextlib
|
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path, PurePath
|
|
5
|
-
from typing import Annotated, Any, Literal
|
|
5
|
+
from typing import Annotated, Any, Literal, cast
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
@@ -27,6 +27,35 @@ def ensure_docker_rm(command: str, args: list[str]) -> list[str]:
|
|
|
27
27
|
return [args[0], "--rm", *args[1:]]
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def apply_docker_rm_to_mcp_config_dict(config: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
"""Inject ``--rm`` into stdio docker/podman servers when loading mcp.json (mcpext-3)."""
|
|
32
|
+
servers = config.get("mcpServers")
|
|
33
|
+
if not isinstance(servers, dict):
|
|
34
|
+
return config
|
|
35
|
+
typed_servers = cast(dict[str, Any], servers)
|
|
36
|
+
for raw_server in typed_servers.values():
|
|
37
|
+
if not isinstance(raw_server, dict):
|
|
38
|
+
continue
|
|
39
|
+
server = cast(dict[str, Any], raw_server)
|
|
40
|
+
command = server.get("command")
|
|
41
|
+
if not isinstance(command, str):
|
|
42
|
+
continue
|
|
43
|
+
raw_args = server.get("args")
|
|
44
|
+
args: list[str] = []
|
|
45
|
+
if isinstance(raw_args, list):
|
|
46
|
+
args = [str(item) for item in cast(list[object], raw_args)]
|
|
47
|
+
server["args"] = ensure_docker_rm(command, args)
|
|
48
|
+
return config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def prepare_mcp_config_dict(config: dict[str, Any]) -> dict[str, Any]:
|
|
52
|
+
"""Apply portable MCP config fixes before validation (docker ``--rm``, name normalization)."""
|
|
53
|
+
from pythinker_code.utils.mcp_names import normalize_mcp_servers_in_config
|
|
54
|
+
|
|
55
|
+
config = apply_docker_rm_to_mcp_config_dict(config)
|
|
56
|
+
return normalize_mcp_servers_in_config(config)
|
|
57
|
+
|
|
58
|
+
|
|
30
59
|
def get_global_mcp_config_file() -> Path:
|
|
31
60
|
"""Get the global MCP config file path."""
|
|
32
61
|
from pythinker_code.share import get_share_dir
|
|
@@ -39,6 +68,8 @@ def _load_mcp_config() -> dict[str, Any]:
|
|
|
39
68
|
from fastmcp.mcp_config import MCPConfig
|
|
40
69
|
from pydantic import ValidationError
|
|
41
70
|
|
|
71
|
+
from pythinker_code.exception import MCPConfigError
|
|
72
|
+
|
|
42
73
|
mcp_file = get_global_mcp_config_file()
|
|
43
74
|
if not mcp_file.exists():
|
|
44
75
|
return {"mcpServers": {}}
|
|
@@ -52,7 +83,30 @@ def _load_mcp_config() -> dict[str, Any]:
|
|
|
52
83
|
except ValidationError as e:
|
|
53
84
|
raise typer.BadParameter(f"Invalid MCP config in '{mcp_file}': {e}") from e
|
|
54
85
|
|
|
55
|
-
|
|
86
|
+
try:
|
|
87
|
+
return prepare_mcp_config_dict(config)
|
|
88
|
+
except MCPConfigError as e:
|
|
89
|
+
raise typer.BadParameter(str(e)) from e
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _resolve_mcp_server_key(name: str, servers: dict[str, Any]) -> str:
|
|
93
|
+
"""Resolve a user-supplied MCP server name to the stored config key."""
|
|
94
|
+
from pythinker_code.exception import MCPConfigError
|
|
95
|
+
from pythinker_code.utils.mcp_names import normalize_mcp_server_name
|
|
96
|
+
|
|
97
|
+
if name in servers:
|
|
98
|
+
return name
|
|
99
|
+
try:
|
|
100
|
+
normalized = normalize_mcp_server_name(name)
|
|
101
|
+
except MCPConfigError as exc:
|
|
102
|
+
typer.echo(str(exc), err=True)
|
|
103
|
+
raise typer.Exit(code=1) from exc
|
|
104
|
+
if normalized in servers:
|
|
105
|
+
if normalized != name:
|
|
106
|
+
typer.echo(f"Resolved MCP server '{name}' to '{normalized}'.")
|
|
107
|
+
return normalized
|
|
108
|
+
typer.echo(f"MCP server '{name}' not found.", err=True)
|
|
109
|
+
raise typer.Exit(code=1)
|
|
56
110
|
|
|
57
111
|
|
|
58
112
|
def _save_mcp_config(config: dict[str, Any]) -> None:
|
|
@@ -71,18 +125,23 @@ def _save_mcp_config(config: dict[str, Any]) -> None:
|
|
|
71
125
|
fh.write(payload)
|
|
72
126
|
|
|
73
127
|
|
|
74
|
-
def
|
|
75
|
-
|
|
128
|
+
def _mcp_servers_from_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
129
|
+
raw_servers = config.get("mcpServers", {})
|
|
130
|
+
if isinstance(raw_servers, dict):
|
|
131
|
+
return cast(dict[str, Any], raw_servers)
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_mcp_server(name: str, *, require_remote: bool = False) -> tuple[str, dict[str, Any]]:
|
|
136
|
+
"""Get MCP server config by name (accepts raw or normalized keys)."""
|
|
76
137
|
config = _load_mcp_config()
|
|
77
|
-
servers = config
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
raise typer.Exit(code=1)
|
|
81
|
-
server = servers[name]
|
|
138
|
+
servers = _mcp_servers_from_config(config)
|
|
139
|
+
stored_name = _resolve_mcp_server_key(name, servers)
|
|
140
|
+
server = cast(dict[str, Any], servers[stored_name])
|
|
82
141
|
if require_remote and "url" not in server:
|
|
83
|
-
typer.echo(f"MCP server '{
|
|
142
|
+
typer.echo(f"MCP server '{stored_name}' is not a remote server.", err=True)
|
|
84
143
|
raise typer.Exit(code=1)
|
|
85
|
-
return server
|
|
144
|
+
return stored_name, server
|
|
86
145
|
|
|
87
146
|
|
|
88
147
|
def _parse_key_value_pairs(
|
|
@@ -171,8 +230,18 @@ def mcp_add(
|
|
|
171
230
|
] = None,
|
|
172
231
|
):
|
|
173
232
|
"""Add an MCP server."""
|
|
233
|
+
from pythinker_code.exception import MCPConfigError
|
|
234
|
+
from pythinker_code.utils.mcp_names import normalize_mcp_server_name
|
|
235
|
+
|
|
174
236
|
config = _load_mcp_config()
|
|
175
237
|
server_args = server_args or []
|
|
238
|
+
try:
|
|
239
|
+
stored_name = normalize_mcp_server_name(name)
|
|
240
|
+
except MCPConfigError as exc:
|
|
241
|
+
typer.echo(str(exc), err=True)
|
|
242
|
+
raise typer.Exit(code=1) from exc
|
|
243
|
+
if stored_name != name:
|
|
244
|
+
typer.echo(f"Normalized MCP server name '{name}' to '{stored_name}'.")
|
|
176
245
|
|
|
177
246
|
if transport not in {"stdio", "http"}:
|
|
178
247
|
typer.echo(f"Unsupported transport: {transport}.", err=True)
|
|
@@ -221,9 +290,12 @@ def mcp_add(
|
|
|
221
290
|
|
|
222
291
|
if "mcpServers" not in config:
|
|
223
292
|
config["mcpServers"] = {}
|
|
224
|
-
config["mcpServers"]
|
|
293
|
+
if stored_name in config["mcpServers"]:
|
|
294
|
+
typer.echo(f"MCP server '{stored_name}' already exists.", err=True)
|
|
295
|
+
raise typer.Exit(code=1)
|
|
296
|
+
config["mcpServers"][stored_name] = server_config
|
|
225
297
|
_save_mcp_config(config)
|
|
226
|
-
typer.echo(f"Added MCP server '{
|
|
298
|
+
typer.echo(f"Added MCP server '{stored_name}' to {get_global_mcp_config_file()}.")
|
|
227
299
|
|
|
228
300
|
|
|
229
301
|
@cli.command("remove")
|
|
@@ -234,11 +306,12 @@ def mcp_remove(
|
|
|
234
306
|
],
|
|
235
307
|
):
|
|
236
308
|
"""Remove an MCP server."""
|
|
237
|
-
_get_mcp_server(name)
|
|
238
309
|
config = _load_mcp_config()
|
|
239
|
-
|
|
310
|
+
servers = _mcp_servers_from_config(config)
|
|
311
|
+
stored_name = _resolve_mcp_server_key(name, servers)
|
|
312
|
+
del config["mcpServers"][stored_name]
|
|
240
313
|
_save_mcp_config(config)
|
|
241
|
-
typer.echo(f"Removed MCP server '{
|
|
314
|
+
typer.echo(f"Removed MCP server '{stored_name}' from {get_global_mcp_config_file()}.")
|
|
242
315
|
|
|
243
316
|
|
|
244
317
|
def _oauth_token_storage(server_url: str) -> Any:
|
|
@@ -306,21 +379,25 @@ def mcp_auth(
|
|
|
306
379
|
import asyncio
|
|
307
380
|
|
|
308
381
|
server = _get_mcp_server(name, require_remote=True)
|
|
309
|
-
|
|
310
|
-
|
|
382
|
+
stored_name, server_config = server
|
|
383
|
+
if server_config.get("auth") != "oauth":
|
|
384
|
+
typer.echo(
|
|
385
|
+
f"MCP server '{stored_name}' does not use OAuth. Add with --auth oauth.",
|
|
386
|
+
err=True,
|
|
387
|
+
)
|
|
311
388
|
raise typer.Exit(code=1)
|
|
312
389
|
|
|
313
390
|
async def _auth() -> None:
|
|
314
391
|
import fastmcp
|
|
315
392
|
|
|
316
|
-
typer.echo(f"Authorizing with '{
|
|
393
|
+
typer.echo(f"Authorizing with '{stored_name}'...")
|
|
317
394
|
typer.echo("A browser window will open for authorization.")
|
|
318
395
|
|
|
319
|
-
client = fastmcp.Client({"mcpServers": {
|
|
396
|
+
client = fastmcp.Client({"mcpServers": {stored_name: server_config}})
|
|
320
397
|
try:
|
|
321
398
|
async with client:
|
|
322
399
|
tools = await client.list_tools()
|
|
323
|
-
typer.echo(f"Successfully authorized with '{
|
|
400
|
+
typer.echo(f"Successfully authorized with '{stored_name}'.")
|
|
324
401
|
typer.echo(f"Available tools: {len(tools)}")
|
|
325
402
|
except Exception as e:
|
|
326
403
|
typer.echo(f"Authorization failed: {type(e).__name__}: {e}", err=True)
|
|
@@ -337,7 +414,7 @@ def mcp_reset_auth(
|
|
|
337
414
|
],
|
|
338
415
|
):
|
|
339
416
|
"""Reset OAuth authorization for an MCP server (clear cached tokens)."""
|
|
340
|
-
server = _get_mcp_server(name, require_remote=True)
|
|
417
|
+
stored_name, server = _get_mcp_server(name, require_remote=True)
|
|
341
418
|
|
|
342
419
|
try:
|
|
343
420
|
import asyncio
|
|
@@ -349,7 +426,7 @@ def mcp_reset_auth(
|
|
|
349
426
|
await result
|
|
350
427
|
|
|
351
428
|
asyncio.run(_clear())
|
|
352
|
-
typer.echo(f"OAuth tokens cleared for '{
|
|
429
|
+
typer.echo(f"OAuth tokens cleared for '{stored_name}'.")
|
|
353
430
|
except ImportError:
|
|
354
431
|
typer.echo("OAuth support not available.", err=True)
|
|
355
432
|
raise typer.Exit(code=1) from None
|
|
@@ -368,18 +445,18 @@ def mcp_test(
|
|
|
368
445
|
"""Test connection to an MCP server and list available tools."""
|
|
369
446
|
import asyncio
|
|
370
447
|
|
|
371
|
-
server = _get_mcp_server(name)
|
|
448
|
+
stored_name, server = _get_mcp_server(name)
|
|
372
449
|
|
|
373
450
|
async def _test() -> None:
|
|
374
451
|
import fastmcp
|
|
375
452
|
|
|
376
|
-
typer.echo(f"Testing connection to '{
|
|
377
|
-
client = fastmcp.Client({"mcpServers": {
|
|
453
|
+
typer.echo(f"Testing connection to '{stored_name}'...")
|
|
454
|
+
client = fastmcp.Client({"mcpServers": {stored_name: server}})
|
|
378
455
|
|
|
379
456
|
try:
|
|
380
457
|
async with client:
|
|
381
458
|
tools = await client.list_tools()
|
|
382
|
-
typer.echo(f"✓ Connected to '{
|
|
459
|
+
typer.echo(f"✓ Connected to '{stored_name}'")
|
|
383
460
|
typer.echo(f" Available tools: {len(tools)}")
|
|
384
461
|
if tools:
|
|
385
462
|
typer.echo(" Tools:")
|