pythinker-code 2.5.0__py3-none-any.whl → 2.7.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 +37 -3
- pythinker_code/__main__.py +4 -0
- pythinker_code/agents/default/agent.yaml +9 -0
- pythinker_code/agents/default/code_reviewer.yaml +47 -0
- pythinker_code/agents/default/debugger.yaml +35 -0
- pythinker_code/agents/default/security_reviewer.yaml +37 -0
- pythinker_code/agents/default/system.md +14 -2
- pythinker_code/app.py +11 -0
- pythinker_code/auth/github_feedback.py +228 -0
- pythinker_code/cli/_lazy_group.py +24 -0
- pythinker_code/cli/debug.py +11 -0
- pythinker_code/cli/review.py +74 -0
- pythinker_code/cli/secscan.py +11 -0
- pythinker_code/cli/security_scan.py +35 -0
- pythinker_code/config.py +11 -0
- pythinker_code/soul/agent.py +22 -0
- pythinker_code/soul/permission.py +27 -0
- pythinker_code/subagents/discovery.py +234 -0
- pythinker_code/telemetry/otel.py +16 -0
- pythinker_code/ui/shell/__init__.py +54 -3
- pythinker_code/ui/shell/components/bash_execution.py +4 -0
- pythinker_code/ui/shell/components/diff.py +4 -3
- pythinker_code/ui/shell/components/tool_execution.py +6 -2
- pythinker_code/ui/shell/oauth.py +8 -1
- pythinker_code/ui/shell/prompt.py +28 -0
- pythinker_code/ui/shell/slash.py +109 -3
- pythinker_code/ui/shell/tool_renderers/_render_utils.py +19 -0
- pythinker_code/ui/shell/tool_renderers/bash.py +13 -9
- pythinker_code/ui/shell/tool_renderers/edit.py +45 -5
- pythinker_code/ui/shell/tool_renderers/find.py +2 -1
- pythinker_code/ui/shell/tool_renderers/grep.py +2 -1
- pythinker_code/ui/shell/tool_renderers/read.py +2 -1
- pythinker_code/ui/shell/tool_renderers/todo.py +16 -13
- pythinker_code/ui/shell/tool_renderers/web.py +3 -2
- pythinker_code/ui/shell/tool_renderers/write.py +9 -6
- pythinker_code/ui/shell/visualize/_blocks.py +97 -1
- pythinker_code/ui/shell/visualize/_live_view.py +91 -49
- pythinker_code/web/static/assets/{_baseUniq-Bv26EHIE.js → _baseUniq-xopHfKWX.js} +1 -1
- pythinker_code/web/static/assets/{arc-DuCCCcUZ.js → arc-BBJ7V9Gp.js} +1 -1
- pythinker_code/web/static/assets/{architectureDiagram-VXUJARFQ-CVZ131zn.js → architectureDiagram-VXUJARFQ-DwGo6qSm.js} +1 -1
- pythinker_code/web/static/assets/{blockDiagram-VD42YOAC-BAC2VOip.js → blockDiagram-VD42YOAC-B96wqtrk.js} +1 -1
- pythinker_code/web/static/assets/{c4Diagram-YG6GDRKO-9uwamEIP.js → c4Diagram-YG6GDRKO-BAv4oGOt.js} +1 -1
- pythinker_code/web/static/assets/channel-BMhsY12h.js +1 -0
- pythinker_code/web/static/assets/{chunk-4BX2VUAB-BTdhSGW0.js → chunk-4BX2VUAB-xvC1-2wn.js} +1 -1
- pythinker_code/web/static/assets/{chunk-55IACEB6-CYDI0p8Q.js → chunk-55IACEB6-Ci3UbC9h.js} +1 -1
- pythinker_code/web/static/assets/{chunk-B4BG7PRW-b8oi1KW8.js → chunk-B4BG7PRW-YlIqOoyf.js} +1 -1
- pythinker_code/web/static/assets/{chunk-DI55MBZ5-DGaf6dom.js → chunk-DI55MBZ5-CiKvkLSV.js} +1 -1
- pythinker_code/web/static/assets/{chunk-FMBD7UC4-C1R9DMCj.js → chunk-FMBD7UC4-6L3DZcgX.js} +1 -1
- pythinker_code/web/static/assets/{chunk-QN33PNHL-DjSDLitQ.js → chunk-QN33PNHL--uAxWJyr.js} +1 -1
- pythinker_code/web/static/assets/{chunk-QZHKN3VN-_zmK8SCU.js → chunk-QZHKN3VN-BgsfDfGl.js} +1 -1
- pythinker_code/web/static/assets/{chunk-TZMSLE5B-DURAXY_D.js → chunk-TZMSLE5B-CmVX5blB.js} +1 -1
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-xLNcUMaH.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-xLNcUMaH.js +1 -0
- pythinker_code/web/static/assets/clone-D4ji5nla.js +1 -0
- pythinker_code/web/static/assets/{code-block-IT6T5CEO-DL4aF17r.js → code-block-IT6T5CEO-DOvUTd9H.js} +1 -1
- pythinker_code/web/static/assets/{cose-bilkent-S5V4N54A-BuUIleVc.js → cose-bilkent-S5V4N54A-CnxamR9p.js} +1 -1
- pythinker_code/web/static/assets/{cytoscape.esm-C-OXuR4H.js → cytoscape.esm-JliMJb2r.js} +1 -1
- pythinker_code/web/static/assets/{dagre-6UL2VRFP-CFMQD_BU.js → dagre-6UL2VRFP-CZPQL3_X.js} +1 -1
- pythinker_code/web/static/assets/{diagram-PSM6KHXK-BlZG6Knx.js → diagram-PSM6KHXK-BdkuXkbh.js} +1 -1
- pythinker_code/web/static/assets/{diagram-QEK2KX5R-Bx6pGAz_.js → diagram-QEK2KX5R-B_gzBcDN.js} +1 -1
- pythinker_code/web/static/assets/{diagram-S2PKOQOG-C3k3j5WT.js → diagram-S2PKOQOG-C9KMyyaT.js} +1 -1
- pythinker_code/web/static/assets/{erDiagram-Q2GNP2WA-ZXF9DMQm.js → erDiagram-Q2GNP2WA-BMG5oLO5.js} +1 -1
- pythinker_code/web/static/assets/{flowDiagram-NV44I4VS-BbSLxWgp.js → flowDiagram-NV44I4VS-ByMcd6Qs.js} +1 -1
- pythinker_code/web/static/assets/{ganttDiagram-JELNMOA3-NOlUrkQp.js → ganttDiagram-JELNMOA3-uIly3nqs.js} +1 -1
- pythinker_code/web/static/assets/{gitGraphDiagram-NY62KEGX-BAlT86AC.js → gitGraphDiagram-NY62KEGX-DMlBqiTw.js} +1 -1
- pythinker_code/web/static/assets/{graph-BzHVPchG.js → graph-B2e5iBNo.js} +1 -1
- pythinker_code/web/static/assets/{index-CnM44gk-.js → index-ChRG_K1c.js} +1 -1
- pythinker_code/web/static/assets/{index-Cm_lwIyA.js → index-DgWvtkAz.js} +2 -2
- pythinker_code/web/static/assets/{index-Cqg-K1YV.js → index-XVss4lOI.js} +1 -1
- pythinker_code/web/static/assets/{infoDiagram-WHAUD3N6-DBX3JCDO.js → infoDiagram-WHAUD3N6-CihjcIPb.js} +1 -1
- pythinker_code/web/static/assets/{journeyDiagram-XKPGCS4Q-yaCqhNqw.js → journeyDiagram-XKPGCS4Q-D8UHxaOf.js} +1 -1
- pythinker_code/web/static/assets/{kanban-definition-3W4ZIXB7-Cw2loPy6.js → kanban-definition-3W4ZIXB7-DMJJ5np4.js} +1 -1
- pythinker_code/web/static/assets/{layout-DC8qmv-q.js → layout-DCD-kyE5.js} +1 -1
- pythinker_code/web/static/assets/{linear-Bpra1Fqc.js → linear-D32cHhqF.js} +1 -1
- pythinker_code/web/static/assets/{mermaid-VLURNSYL-BvHbNBJJ.js → mermaid-VLURNSYL-BdQ-I_9q.js} +7 -7
- pythinker_code/web/static/assets/{mermaid.core-QA4yHgUs.js → mermaid.core-TDrBZ18i.js} +5 -5
- pythinker_code/web/static/assets/{min-sLtZymTB.js → min-BLYTe2ei.js} +1 -1
- pythinker_code/web/static/assets/{mindmap-definition-VGOIOE7T-BycLAQjs.js → mindmap-definition-VGOIOE7T-D4dVU6Wl.js} +1 -1
- pythinker_code/web/static/assets/{pieDiagram-ADFJNKIX-CpFv1-B_.js → pieDiagram-ADFJNKIX-BPhhXGkK.js} +1 -1
- pythinker_code/web/static/assets/{quadrantDiagram-AYHSOK5B-C8HyfNyW.js → quadrantDiagram-AYHSOK5B-Bw33P7le.js} +1 -1
- pythinker_code/web/static/assets/{requirementDiagram-UZGBJVZJ-DyTuOubd.js → requirementDiagram-UZGBJVZJ-BJhppvmZ.js} +1 -1
- pythinker_code/web/static/assets/{sankeyDiagram-TZEHDZUN-BlUGUAE1.js → sankeyDiagram-TZEHDZUN-B2QsrhB7.js} +1 -1
- pythinker_code/web/static/assets/{sequenceDiagram-WL72ISMW-jqMJUHqm.js → sequenceDiagram-WL72ISMW-D-zE9VAU.js} +1 -1
- pythinker_code/web/static/assets/{stateDiagram-FKZM4ZOC-zShF28En.js → stateDiagram-FKZM4ZOC-fXBn8fR5.js} +1 -1
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-Uh1qibgy.js +1 -0
- pythinker_code/web/static/assets/{timeline-definition-IT6M3QCI-Cvekw0Hb.js → timeline-definition-IT6M3QCI-QJcB1GYA.js} +1 -1
- pythinker_code/web/static/assets/{treemap-KMMF4GRG-DuFTYtWm.js → treemap-KMMF4GRG-BIsZeCJI.js} +1 -1
- pythinker_code/web/static/assets/{xychartDiagram-PRI3JC2R-BVeBFpa0.js → xychartDiagram-PRI3JC2R-D23m9ArK.js} +1 -1
- pythinker_code/web/static/index.html +1 -1
- {pythinker_code-2.5.0.dist-info → pythinker_code-2.7.0.dist-info}/METADATA +56 -14
- pythinker_code-2.7.0.dist-info/RECORD +789 -0
- {pythinker_code-2.5.0.dist-info → pythinker_code-2.7.0.dist-info}/WHEEL +2 -2
- pythinker_code/web/static/assets/channel-aVyB491s.js +0 -1
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-cvkDF0Mx.js +0 -1
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-cvkDF0Mx.js +0 -1
- pythinker_code/web/static/assets/clone-DQNyDB_s.js +0 -1
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-lllXGHDb.js +0 -1
- pythinker_code-2.5.0.dist-info/RECORD +0 -780
- {pythinker_code-2.5.0.dist-info → pythinker_code-2.7.0.dist-info}/entry_points.txt +0 -0
- {pythinker_code-2.5.0.dist-info → pythinker_code-2.7.0.dist-info}/licenses/LICENSE +0 -0
- {pythinker_code-2.5.0.dist-info → pythinker_code-2.7.0.dist-info}/licenses/NOTICE +0 -0
pythinker_code/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 2.7.0 (2026-05-21)
|
|
6
|
+
|
|
7
|
+
First-class review-first workflows: code review, security scanning, root-cause debugging, PR artifact helpers, and Reviewflow stateful review/fix workflows.
|
|
8
|
+
|
|
9
|
+
- Added the `pythinker-review` workspace package and wired it into the root `pythinker-code` package via the pinned `pythinker-review==0.1.0` dependency.
|
|
10
|
+
- Added `pythinker review`, `pythinker secscan`, `pythinker security-scan`, and `pythinker debug` command surfaces, backed by standalone `pythinker-review`, `pythinker-secscan`, `pythinker-security-scan`, and `pythinker-debug` console scripts.
|
|
11
|
+
- Added read-only PR artifact/helper commands: `describe`, `improve`/`suggest`, `ask`, `ask-line`, `labels`, `changelog`, `docs`, `compliance`, `help-docs`, `similar-issues`, `tools`, and `config`.
|
|
12
|
+
- Added Reviewflow workflow commands and state models for `init`, `map`, `review`, `report`, `next`, `show --finding`, `triage`, `revalidate`, `fix`, `open-pr`, `ci`, and `doctor`.
|
|
13
|
+
- Added the `code-reviewer`, `security-reviewer`, and `debugger` subagent roles for interactive Pythinker sessions.
|
|
14
|
+
- Hardened review output validation so unsafe paths, stale line ranges, mismatched evidence snippets, malformed model JSON, worker failures, and timeouts fail closed unless `--allow-partial` is explicitly used.
|
|
15
|
+
- Refreshed README release notes with the 2.7.0 review/security/debug feature set and the pinned `pythinker-code==2.7.0` upgrade snippet required by the release gate.
|
|
16
|
+
|
|
17
|
+
Upgrade with `pythinker update` or `pip install --upgrade pythinker-code==2.7.0`.
|
|
18
|
+
|
|
19
|
+
## 2.6.0 (2026-05-13)
|
|
20
|
+
|
|
21
|
+
Packaging fix: pin `pythinker-core[contrib]==1.1.0` so the Kimi K2.x / DeepSeek strict-interleaved reasoning-replay fix reaches PyPI installs.
|
|
22
|
+
|
|
23
|
+
### Why 2.6.0 lands the same day as 2.5.0
|
|
24
|
+
|
|
25
|
+
The runtime fix for the strict-interleaved `thinking is enabled but reasoning_content is missing in assistant tool call message at index N` rejection landed in the `pythinker-core` source tree on 2026-05-11 (released in `pythinker-code` 2.4.0 source), but **the published `pythinker-core==1.0.0` on PyPI predates that change** (uploaded 2026-05-07). `pythinker-code` 2.4.0 and 2.5.0 both pinned `pythinker-core[contrib]==1.0.0`, so PyPI users on Kimi K2.5 / K2.6 / DeepSeek through OpenCode Go (and elsewhere) kept hitting the bug even on the latest CLI. Reported in [#37](https://github.com/mohamed-elkholy95/Pythinker-Code/issues/37).
|
|
26
|
+
|
|
27
|
+
### The fix
|
|
28
|
+
|
|
29
|
+
- **`pythinker-core` bumped to 1.1.0** (published independently on PyPI) — carries the strict-interleaved replay logic that emits `reasoning_content` on every assistant turn for `kimi-k2*` / `deepseek*` models, falling back to `extract_text()` then `"[reasoning unavailable]"` when no `ThinkPart` was captured.
|
|
30
|
+
- **Root pin updated to `pythinker-core[contrib]==1.1.0`** in `pyproject.toml`. New installs of `pip install --upgrade pythinker-code==2.6.0` (and `uv tool install`/`upgrade`) get the fix automatically.
|
|
31
|
+
- See `packages/pythinker-core/CHANGELOG.md` for the full pythinker-core 1.1.0 entry.
|
|
32
|
+
|
|
33
|
+
### No app-level behavior changes vs 2.5.0
|
|
34
|
+
|
|
35
|
+
Everything that landed in 2.5.0 is still there. The 2.6.0 diff is exclusively the version bump and the upstream-dep pin; existing 2.5.0 users **must** upgrade to 2.6.0 if they use Kimi K2.x, DeepSeek, or any other strict-interleaved-thinking model through an OpenAI-compatible provider.
|
|
36
|
+
|
|
37
|
+
Upgrade with `pythinker update` or `pip install --upgrade pythinker-code==2.6.0`.
|
|
38
|
+
|
|
5
39
|
## 2.5.0 (2026-05-13)
|
|
6
40
|
|
|
7
41
|
bk_box_main coding-agent runtime port, Windows self-upgrade fix, FetchURL SSRF hardening, and a broad reliability/security pass.
|
|
@@ -64,7 +98,7 @@ Upgrade with `pythinker update` or `pip install --upgrade pythinker-code==2.5.0`
|
|
|
64
98
|
|
|
65
99
|
## 2.4.0 (2026-05-11)
|
|
66
100
|
|
|
67
|
-
Subagent roles overhaul,
|
|
101
|
+
Subagent roles overhaul, Kimi K2 provider support, and a ripgrep-free Grep fallback.
|
|
68
102
|
|
|
69
103
|
- New built-in subagents under `src/pythinker_code/agents/default/`:
|
|
70
104
|
- `implementer.yaml` — scoped code changes with minimum surrounding edits and a quick verification pass.
|
|
@@ -73,8 +107,8 @@ Subagent roles overhaul, Moonshot/Kimi K2 provider support, and a ripgrep-free G
|
|
|
73
107
|
- `coder.yaml`, `explore.yaml`, and `plan.yaml` now emit a standard `### SUMMARY / EVIDENCE / CHANGES / RISKS / BLOCKERS` response contract so the parent agent can consume subagent output without re-parsing prose.
|
|
74
108
|
- `agent.yaml` registers the three new roles; `tools/agent/description.md` documents the Scout → Plan → Implement → Review → Verify workflow and the parallel review/verification pattern.
|
|
75
109
|
- `agents/default/system.md`: adds decomposition guidance (preview → todo list → parallel chunks), enforces post-tool-call verification before acting on results, and tells the agent to cross-check at least one load-bearing subagent finding before editing from it.
|
|
76
|
-
- Kimi K2.5 / K2.6
|
|
77
|
-
- `packages/pythinker-core/.../chat_provider/pythinker.py`: always emit `reasoning_content` on assistant tool-call replays so
|
|
110
|
+
- Kimi K2.5 / K2.6 and other strict interleaved-thinking providers:
|
|
111
|
+
- `packages/pythinker-core/.../chat_provider/pythinker.py`: always emit `reasoning_content` on assistant tool-call replays so the strict "thinking is enabled but reasoning_content is missing in assistant tool call message at index N" error no longer trips multi-step tool flows.
|
|
78
112
|
- `packages/pythinker-core/.../contrib/chat_provider/openai_legacy.py`: replay reasoning metadata on every assistant turn for `kimi-k2*` / `deepseek*` models (falls back to the assistant text or `"[reasoning unavailable]"` when reasoning content was not retained).
|
|
79
113
|
- `src/pythinker_code/llm.py`: route Kimi K2 thinking through the provider-specific `extra_body={"thinking": {"type": "enabled"|"disabled"}}` body field instead of OpenAI's `reasoning_effort` (which Kimi ignores), and persist `LLM.thinking` across `clone_llm_with_model_alias` so model switches preserve the user's thinking choice.
|
|
80
114
|
- `tools/file/grep_local.py`:
|
pythinker_code/__main__.py
CHANGED
|
@@ -46,6 +46,10 @@ Commands:
|
|
|
46
46
|
export Export session data.
|
|
47
47
|
mcp Manage MCP server configurations.
|
|
48
48
|
plugin Manage plugins.
|
|
49
|
+
review Diff-focused code review (delegates to pythinker-review).
|
|
50
|
+
secscan Diff-focused security review (delegates to pythinker-review).
|
|
51
|
+
security-scan Repo-wide Pythinker Security Scan pipeline (Python-native).
|
|
52
|
+
debug Failure/log root-cause analysis (delegates to pythinker-review).
|
|
49
53
|
update Check for and install Pythinker CLI updates.
|
|
50
54
|
vis Run Pythinker Agent Tracing Visualizer.
|
|
51
55
|
web Run Pythinker CLI web interface.
|
|
@@ -29,6 +29,12 @@ agent:
|
|
|
29
29
|
coder:
|
|
30
30
|
path: ./coder.yaml
|
|
31
31
|
description: "Good at general software engineering tasks."
|
|
32
|
+
code-reviewer:
|
|
33
|
+
path: ./code_reviewer.yaml
|
|
34
|
+
description: "Diff-focused code review with severity-scored findings."
|
|
35
|
+
debugger:
|
|
36
|
+
path: ./debugger.yaml
|
|
37
|
+
description: "Failure/log/stack-trace root-cause analysis with reproduction evidence."
|
|
32
38
|
explore:
|
|
33
39
|
path: ./explore.yaml
|
|
34
40
|
description: "Fast codebase exploration with prompt-enforced read-only behavior."
|
|
@@ -38,6 +44,9 @@ agent:
|
|
|
38
44
|
review:
|
|
39
45
|
path: ./review.yaml
|
|
40
46
|
description: "Read-only code review with severity-scored findings."
|
|
47
|
+
security-reviewer:
|
|
48
|
+
path: ./security_reviewer.yaml
|
|
49
|
+
description: "Diff-focused security review with validated findings."
|
|
41
50
|
implementer:
|
|
42
51
|
path: ./implementer.yaml
|
|
43
52
|
description: "Scoped implementation with minimal edits and verification."
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
agent:
|
|
3
|
+
extend: ./agent.yaml
|
|
4
|
+
system_prompt_args:
|
|
5
|
+
ROLE_ADDITIONAL: |
|
|
6
|
+
You are running as the Pythinker code-reviewer subagent. The parent agent is your caller and can only see your final response.
|
|
7
|
+
|
|
8
|
+
Role and scope:
|
|
9
|
+
- Perform read-only, evidence-first review of the current repository diff.
|
|
10
|
+
- Use `pythinker review diff` by default; add `--with-security` when the parent requests security coverage.
|
|
11
|
+
- Use `pythinker review describe`, `suggest`/`improve`, `ask`, `ask-line`, `labels`, `changelog`, `docs`, `compliance`, `help-docs`, `similar-issues`, `tools`, or `config` only when the parent explicitly asks for that artifact/helper.
|
|
12
|
+
- For code-reviewr parity requests, prefer local read-only options such as `--labels-file`, `--extra-instructions`, `--best-practices-file`, `--min-score`, `--docs-style`, `--symbol`, `--pr-url`, and `--issues-dir` instead of provider publishing.
|
|
13
|
+
- Do not edit files, commit, stage, push, approve, merge, or publish provider comments.
|
|
14
|
+
|
|
15
|
+
Tool policy:
|
|
16
|
+
- Prefer `pythinker review diff --format json --no-save` so results are structured and do not write run state.
|
|
17
|
+
- If persistence is requested, omit `--no-save` and report where run state was written.
|
|
18
|
+
- If the requested base ref fails, report the exact blocker instead of guessing another branch unless the parent gave fallback instructions.
|
|
19
|
+
- Read files only to verify a load-bearing finding or command failure.
|
|
20
|
+
|
|
21
|
+
Review discipline:
|
|
22
|
+
- Flag only issues introduced or made reachable by the diff.
|
|
23
|
+
- Prefer no finding over vague speculation.
|
|
24
|
+
- Treat malformed model output, validation errors, empty diffs, and missing base refs as blockers, not successful reviews.
|
|
25
|
+
- Surface false-positive risks and coverage limits clearly.
|
|
26
|
+
|
|
27
|
+
Final response contract:
|
|
28
|
+
### SUMMARY
|
|
29
|
+
One paragraph: command run, number of findings/artifacts, top severity or most important result.
|
|
30
|
+
### EVIDENCE
|
|
31
|
+
Bullet list of `<file>:<line> [severity] <rule_id> — <title>` for findings, or concise artifact bullets for non-finding commands. Top 10 max.
|
|
32
|
+
### CHANGES
|
|
33
|
+
None.
|
|
34
|
+
### RISKS
|
|
35
|
+
False-positive risks, partial context, skipped files, or `None observed.`.
|
|
36
|
+
### BLOCKERS
|
|
37
|
+
Anything that prevented a clean run (exit code 2/3/4, base ref missing, malformed output, validation errors), or `None.`.
|
|
38
|
+
when_to_use: |
|
|
39
|
+
Use to run a read-only diff-focused code review or code-reviewr-derived PR artifact workflow on the current branch.
|
|
40
|
+
allowed_tools:
|
|
41
|
+
- "pythinker_code.tools.shell:Shell"
|
|
42
|
+
- "pythinker_code.tools.file:ReadFile"
|
|
43
|
+
- "pythinker_code.tools.file:Grep"
|
|
44
|
+
exclude_tools:
|
|
45
|
+
- "pythinker_code.tools.file:WriteFile"
|
|
46
|
+
- "pythinker_code.tools.file:StrReplaceFile"
|
|
47
|
+
subagents:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
agent:
|
|
3
|
+
extend: ./agent.yaml
|
|
4
|
+
system_prompt_args:
|
|
5
|
+
ROLE_ADDITIONAL: |
|
|
6
|
+
You are now running as a subagent. All `user` messages are sent by the main agent. The main agent cannot see your context, only your last message. Treat the parent agent as your caller. Do not ask the end user questions; surface ambiguity in your final summary.
|
|
7
|
+
|
|
8
|
+
You are a root-cause debugger. Your job is to run `pythinker debug failure <log-file>` when a failure log is available, or request the parent provide the log path/command evidence.
|
|
9
|
+
|
|
10
|
+
Operating rules:
|
|
11
|
+
- Read-only by convention. Do not edit source files.
|
|
12
|
+
- Focus on reproduction evidence, changed-file correlation, likely root cause, and minimal next action.
|
|
13
|
+
- Default to `--format json` and translate the result into the structured response block below.
|
|
14
|
+
|
|
15
|
+
Final response contract:
|
|
16
|
+
### SUMMARY
|
|
17
|
+
One paragraph: likely root cause, confidence, and first recommended action.
|
|
18
|
+
### EVIDENCE
|
|
19
|
+
Bullet list of log/stack/diff evidence with file:line when available.
|
|
20
|
+
### CHANGES
|
|
21
|
+
None.
|
|
22
|
+
### RISKS
|
|
23
|
+
Ambiguities, missing reproduction context, or `None observed.`.
|
|
24
|
+
### BLOCKERS
|
|
25
|
+
Missing log path, command, environment, or `None.`.
|
|
26
|
+
when_to_use: |
|
|
27
|
+
Use for failing tests, stack traces, runtime errors, flaky failures, or debugging requests where root cause should be found before editing code.
|
|
28
|
+
allowed_tools:
|
|
29
|
+
- "pythinker_code.tools.shell:Shell"
|
|
30
|
+
- "pythinker_code.tools.file:ReadFile"
|
|
31
|
+
- "pythinker_code.tools.file:Grep"
|
|
32
|
+
exclude_tools:
|
|
33
|
+
- "pythinker_code.tools.file:WriteFile"
|
|
34
|
+
- "pythinker_code.tools.file:StrReplaceFile"
|
|
35
|
+
subagents:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
agent:
|
|
3
|
+
extend: ./agent.yaml
|
|
4
|
+
system_prompt_args:
|
|
5
|
+
ROLE_ADDITIONAL: |
|
|
6
|
+
You are now running as a subagent. All `user` messages are sent by the main agent. The main agent cannot see your context, only your last message. Treat the parent agent as your caller. Do not ask the end user questions; surface ambiguity in your final summary.
|
|
7
|
+
|
|
8
|
+
You are a security reviewer. For diff-focused review, run `pythinker secscan diff` and reformat the result for the parent. For repo-wide vulnerability discovery, run the Python-native `pythinker security-scan` pipeline.
|
|
9
|
+
|
|
10
|
+
Operating rules:
|
|
11
|
+
- Read-only by convention. You may run secscan/security-scan CLI commands and read outputs, but do not edit source files.
|
|
12
|
+
- Diff mode default: `pythinker secscan diff --format json --no-save`.
|
|
13
|
+
- Repo-wide mode default: `pythinker security-scan scan --json`, then only run `pythinker security-scan process` when the parent explicitly asks for model-backed investigation.
|
|
14
|
+
- Use `--fail-on critical` for triage runs unless the parent specifies otherwise.
|
|
15
|
+
- Translate JSON output into the structured response block below.
|
|
16
|
+
|
|
17
|
+
Final response contract:
|
|
18
|
+
### SUMMARY
|
|
19
|
+
One paragraph: number and severity of security findings, what the parent should fix first.
|
|
20
|
+
### EVIDENCE
|
|
21
|
+
Bullet list of `<file>:<line> [severity] <rule_id> — <title>`, top 10.
|
|
22
|
+
### CHANGES
|
|
23
|
+
None.
|
|
24
|
+
### RISKS
|
|
25
|
+
False-positive risks, missing context, or coverage gaps; or `None observed.`.
|
|
26
|
+
### BLOCKERS
|
|
27
|
+
Anything that prevented a clean run (exit 3/4, base ref missing), or `None.`.
|
|
28
|
+
when_to_use: |
|
|
29
|
+
Use to run a diff-only security review on the current branch. Can run in parallel with `code-reviewer`.
|
|
30
|
+
allowed_tools:
|
|
31
|
+
- "pythinker_code.tools.shell:Shell"
|
|
32
|
+
- "pythinker_code.tools.file:ReadFile"
|
|
33
|
+
- "pythinker_code.tools.file:Grep"
|
|
34
|
+
exclude_tools:
|
|
35
|
+
- "pythinker_code.tools.file:WriteFile"
|
|
36
|
+
- "pythinker_code.tools.file:StrReplaceFile"
|
|
37
|
+
subagents:
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
You are Pythinker
|
|
1
|
+
You are Pythinker — a think-first software engineering agent running on the user's computer. Before you write code, you read code.
|
|
2
2
|
|
|
3
|
-
Your
|
|
3
|
+
Your identity, in order of priority:
|
|
4
|
+
|
|
5
|
+
1. **Code reviewer.** Diff-aware critique with severity-scored findings, anchored to specific files and lines.
|
|
6
|
+
2. **Security & vulnerability scanner.** Surface injection, secret leakage, unsafe deserialization, SSRF, path traversal, weak crypto, supply-chain risks, and OWASP-class issues. Validate before reporting.
|
|
7
|
+
3. **Root-cause diagnostician.** Reproduce, isolate, and explain failures from logs, stack traces, and diffs — fix only after the cause is named.
|
|
8
|
+
4. **Code creator.** Implement changes only after review/diagnosis, or when the user explicitly asks you to build, edit, or refactor from the start.
|
|
9
|
+
|
|
10
|
+
You still have the full coding toolset and use it decisively when asked. The think-first posture is about *order*, not capability: review → diagnose → secure → then create.
|
|
4
11
|
|
|
5
12
|
${ROLE_ADDITIONAL}
|
|
6
13
|
|
|
14
|
+
Product posture (strong): for any ambiguous engineering request, default to evidence-first review, security diagnosis, or root-cause analysis before editing code. Inspect evidence and produce findings/recommendations first. Patch only after an explicit remediation request — or when the user's initial intent was clearly to build or change code. Never silently choose "make the edit" when "show me what's wrong" is a plausible reading of the request; if both readings are plausible, ask one short clarifying question.
|
|
15
|
+
|
|
16
|
+
When you do produce findings, prefer the existing reviewer/scanner subagents over ad-hoc analysis: `code-reviewer` for diff critique, `security-reviewer` for vulnerability validation, `debugger` for failure root-causing, `review`/`explore`/`plan` for read-only passes. Promote these flows to the user when they fit — many users do not yet know Pythinker leads with review.
|
|
17
|
+
|
|
18
|
+
|
|
7
19
|
# Prompt and Tool Use
|
|
8
20
|
|
|
9
21
|
The user's messages may contain questions and/or task descriptions in natural language, code snippets, logs, file paths, or other forms of information. Read them, understand them and do what the user requested. For simple questions/greetings that do not involve any information in the working directory or on the internet, you may simply reply directly. For anything else, default to taking action with tools. When the request could be interpreted as either a question to answer or a task to complete, treat it as a task.
|
pythinker_code/app.py
CHANGED
|
@@ -776,6 +776,17 @@ class PythinkerCLI:
|
|
|
776
776
|
level=WelcomeInfoItem.Level.WARN,
|
|
777
777
|
)
|
|
778
778
|
)
|
|
779
|
+
welcome_info.append(
|
|
780
|
+
WelcomeInfoItem(
|
|
781
|
+
name="Tip",
|
|
782
|
+
value=(
|
|
783
|
+
'Pythinker reviews before it writes. Try "review this diff",'
|
|
784
|
+
' "scan for vulnerabilities", or "find the root cause"'
|
|
785
|
+
" — code edits come after the analysis."
|
|
786
|
+
),
|
|
787
|
+
level=WelcomeInfoItem.Level.INFO,
|
|
788
|
+
)
|
|
789
|
+
)
|
|
779
790
|
welcome_info.append(
|
|
780
791
|
WelcomeInfoItem(
|
|
781
792
|
name="Tip",
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
import webbrowser
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from pythinker_code.auth.oauth import (
|
|
10
|
+
OAuthEvent,
|
|
11
|
+
OAuthRef,
|
|
12
|
+
OAuthToken,
|
|
13
|
+
delete_tokens,
|
|
14
|
+
load_tokens,
|
|
15
|
+
save_tokens,
|
|
16
|
+
)
|
|
17
|
+
from pythinker_code.utils.aiohttp import new_client_session
|
|
18
|
+
from pythinker_code.utils.logging import logger
|
|
19
|
+
|
|
20
|
+
GITHUB_FEEDBACK_OAUTH_KEY = "oauth/github-feedback"
|
|
21
|
+
GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"
|
|
22
|
+
GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
23
|
+
GITHUB_API_URL = "https://api.github.com"
|
|
24
|
+
GITHUB_API_VERSION = "2022-11-28"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitHubFeedbackError(RuntimeError):
|
|
28
|
+
"""GitHub feedback submission failed."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True, frozen=True)
|
|
32
|
+
class GitHubIssue:
|
|
33
|
+
number: int | None
|
|
34
|
+
html_url: str | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(slots=True, frozen=True)
|
|
38
|
+
class _GitHubDeviceAuthorization:
|
|
39
|
+
device_code: str
|
|
40
|
+
user_code: str
|
|
41
|
+
verification_uri: str
|
|
42
|
+
expires_in: int
|
|
43
|
+
interval: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _oauth_ref() -> OAuthRef:
|
|
47
|
+
return OAuthRef(storage="file", key=GITHUB_FEEDBACK_OAUTH_KEY)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_github_feedback_token() -> str | None:
|
|
51
|
+
token = load_tokens(_oauth_ref())
|
|
52
|
+
if token is None or not token.access_token:
|
|
53
|
+
return None
|
|
54
|
+
return token.access_token
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def delete_github_feedback_token() -> None:
|
|
58
|
+
delete_tokens(_oauth_ref())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def _request_device_authorization(client_id: str) -> _GitHubDeviceAuthorization:
|
|
62
|
+
async with (
|
|
63
|
+
new_client_session() as session,
|
|
64
|
+
session.post(
|
|
65
|
+
GITHUB_DEVICE_CODE_URL,
|
|
66
|
+
data={"client_id": client_id, "scope": "public_repo"},
|
|
67
|
+
headers={"Accept": "application/json"},
|
|
68
|
+
) as response,
|
|
69
|
+
):
|
|
70
|
+
data_any: Any = await response.json(content_type=None)
|
|
71
|
+
status = response.status
|
|
72
|
+
if not isinstance(data_any, dict):
|
|
73
|
+
raise GitHubFeedbackError("Unexpected GitHub device authorization response.")
|
|
74
|
+
data = cast(dict[str, Any], data_any)
|
|
75
|
+
if status != 200:
|
|
76
|
+
message = str(data.get("error_description") or data.get("error") or status)
|
|
77
|
+
raise GitHubFeedbackError(f"GitHub device authorization failed: {message}")
|
|
78
|
+
return _GitHubDeviceAuthorization(
|
|
79
|
+
device_code=str(data["device_code"]),
|
|
80
|
+
user_code=str(data["user_code"]),
|
|
81
|
+
verification_uri=str(data["verification_uri"]),
|
|
82
|
+
expires_in=int(data.get("expires_in") or 900),
|
|
83
|
+
interval=max(int(data.get("interval") or 5), 1),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _poll_access_token(
|
|
88
|
+
client_id: str,
|
|
89
|
+
auth: _GitHubDeviceAuthorization,
|
|
90
|
+
) -> OAuthToken:
|
|
91
|
+
interval = auth.interval
|
|
92
|
+
expires_at = time.monotonic() + auth.expires_in
|
|
93
|
+
while time.monotonic() < expires_at:
|
|
94
|
+
await asyncio.sleep(interval)
|
|
95
|
+
async with (
|
|
96
|
+
new_client_session() as session,
|
|
97
|
+
session.post(
|
|
98
|
+
GITHUB_ACCESS_TOKEN_URL,
|
|
99
|
+
data={
|
|
100
|
+
"client_id": client_id,
|
|
101
|
+
"device_code": auth.device_code,
|
|
102
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
103
|
+
},
|
|
104
|
+
headers={"Accept": "application/json"},
|
|
105
|
+
) as response,
|
|
106
|
+
):
|
|
107
|
+
data_any: Any = await response.json(content_type=None)
|
|
108
|
+
status = response.status
|
|
109
|
+
if not isinstance(data_any, dict):
|
|
110
|
+
raise GitHubFeedbackError("Unexpected GitHub token response.")
|
|
111
|
+
data = cast(dict[str, Any], data_any)
|
|
112
|
+
if status != 200:
|
|
113
|
+
message = str(data.get("error_description") or data.get("error") or status)
|
|
114
|
+
raise GitHubFeedbackError(f"GitHub token request failed: {message}")
|
|
115
|
+
access_token = str(data.get("access_token") or "")
|
|
116
|
+
if access_token:
|
|
117
|
+
return OAuthToken(
|
|
118
|
+
access_token=access_token,
|
|
119
|
+
refresh_token=str(data.get("refresh_token") or ""),
|
|
120
|
+
expires_at=float(data.get("expires_at") or 0),
|
|
121
|
+
scope=str(data.get("scope") or ""),
|
|
122
|
+
token_type=str(data.get("token_type") or "bearer"),
|
|
123
|
+
expires_in=float(data.get("expires_in") or 0),
|
|
124
|
+
)
|
|
125
|
+
error_code = str(data.get("error") or "")
|
|
126
|
+
if error_code == "authorization_pending":
|
|
127
|
+
continue
|
|
128
|
+
if error_code == "slow_down":
|
|
129
|
+
interval += 5
|
|
130
|
+
continue
|
|
131
|
+
if error_code == "expired_token":
|
|
132
|
+
raise GitHubFeedbackError("GitHub device code expired.")
|
|
133
|
+
if error_code == "access_denied":
|
|
134
|
+
raise GitHubFeedbackError("GitHub authorization was denied.")
|
|
135
|
+
message = str(data.get("error_description") or error_code or "unknown error")
|
|
136
|
+
raise GitHubFeedbackError(f"GitHub authorization failed: {message}")
|
|
137
|
+
raise GitHubFeedbackError("GitHub device code expired.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def login_github_feedback(
|
|
141
|
+
client_id: str,
|
|
142
|
+
*,
|
|
143
|
+
open_browser: bool = True,
|
|
144
|
+
):
|
|
145
|
+
auth = await _request_device_authorization(client_id)
|
|
146
|
+
yield OAuthEvent(
|
|
147
|
+
"verification_url",
|
|
148
|
+
f"Open {auth.verification_uri} and enter code {auth.user_code}",
|
|
149
|
+
data={"verification_url": auth.verification_uri, "user_code": auth.user_code},
|
|
150
|
+
)
|
|
151
|
+
if open_browser:
|
|
152
|
+
try:
|
|
153
|
+
webbrowser.open(auth.verification_uri)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
logger.warning("Failed to open browser: {error}", error=exc)
|
|
156
|
+
yield OAuthEvent("waiting", "Waiting for GitHub authorization...")
|
|
157
|
+
token = await _poll_access_token(client_id, auth)
|
|
158
|
+
save_tokens(_oauth_ref(), token)
|
|
159
|
+
yield OAuthEvent("success", "GitHub authorization complete.")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _normalize_repo(repo: str) -> str:
|
|
163
|
+
repo = repo.strip().strip("/")
|
|
164
|
+
if repo.count("/") != 1:
|
|
165
|
+
raise GitHubFeedbackError("GitHub feedback repo must be in owner/name form.")
|
|
166
|
+
return repo
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def create_github_issue(
|
|
170
|
+
repo: str,
|
|
171
|
+
token: str,
|
|
172
|
+
*,
|
|
173
|
+
title: str,
|
|
174
|
+
body: str,
|
|
175
|
+
) -> GitHubIssue:
|
|
176
|
+
repo = _normalize_repo(repo)
|
|
177
|
+
async with (
|
|
178
|
+
new_client_session() as session,
|
|
179
|
+
session.post(
|
|
180
|
+
f"{GITHUB_API_URL}/repos/{repo}/issues",
|
|
181
|
+
json={"title": title, "body": body},
|
|
182
|
+
headers={
|
|
183
|
+
"Authorization": f"Bearer {token}",
|
|
184
|
+
"Accept": "application/vnd.github+json",
|
|
185
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
186
|
+
"User-Agent": "pythinker-feedback-cli",
|
|
187
|
+
},
|
|
188
|
+
) as response,
|
|
189
|
+
):
|
|
190
|
+
data_any: Any = await response.json(content_type=None)
|
|
191
|
+
status = response.status
|
|
192
|
+
if not isinstance(data_any, dict):
|
|
193
|
+
raise GitHubFeedbackError("Unexpected GitHub issue response.")
|
|
194
|
+
data = cast(dict[str, Any], data_any)
|
|
195
|
+
if status not in {200, 201}:
|
|
196
|
+
message = str(data.get("message") or status)
|
|
197
|
+
raise GitHubFeedbackError(f"GitHub issue creation failed: {message}")
|
|
198
|
+
number = data.get("number")
|
|
199
|
+
return GitHubIssue(
|
|
200
|
+
number=int(number) if isinstance(number, int) else None,
|
|
201
|
+
html_url=str(data.get("html_url") or "") or None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def star_github_repo(repo: str, token: str) -> None:
|
|
206
|
+
repo = _normalize_repo(repo)
|
|
207
|
+
async with (
|
|
208
|
+
new_client_session() as session,
|
|
209
|
+
session.put(
|
|
210
|
+
f"{GITHUB_API_URL}/user/starred/{repo}",
|
|
211
|
+
headers={
|
|
212
|
+
"Authorization": f"Bearer {token}",
|
|
213
|
+
"Accept": "application/vnd.github+json",
|
|
214
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
215
|
+
"User-Agent": "pythinker-feedback-cli",
|
|
216
|
+
},
|
|
217
|
+
) as response,
|
|
218
|
+
):
|
|
219
|
+
status = response.status
|
|
220
|
+
if status == 204:
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
data_any: Any = await response.json(content_type=None)
|
|
224
|
+
except Exception:
|
|
225
|
+
data_any = {}
|
|
226
|
+
message_payload = cast(dict[str, Any], data_any) if isinstance(data_any, dict) else {}
|
|
227
|
+
message = str(message_payload.get("message") or status)
|
|
228
|
+
raise GitHubFeedbackError(f"GitHub star failed: {message}")
|
|
@@ -18,6 +18,26 @@ class LazySubcommandGroup(typer.core.TyperGroup):
|
|
|
18
18
|
"export": ("pythinker_code.cli.export", "cli", "Export session data."),
|
|
19
19
|
"mcp": ("pythinker_code.cli.mcp", "cli", "Manage MCP server configurations."),
|
|
20
20
|
"plugin": ("pythinker_code.cli.plugin", "cli", "Manage plugins."),
|
|
21
|
+
"review": (
|
|
22
|
+
"pythinker_code.cli.review",
|
|
23
|
+
"cli",
|
|
24
|
+
"Diff-focused code review (delegates to pythinker-review).",
|
|
25
|
+
),
|
|
26
|
+
"secscan": (
|
|
27
|
+
"pythinker_code.cli.secscan",
|
|
28
|
+
"cli",
|
|
29
|
+
"Diff-focused security review (delegates to pythinker-review).",
|
|
30
|
+
),
|
|
31
|
+
"security-scan": (
|
|
32
|
+
"pythinker_code.cli.security_scan",
|
|
33
|
+
"cli",
|
|
34
|
+
"Repo-wide Pythinker Security Scan pipeline (Python-native).",
|
|
35
|
+
),
|
|
36
|
+
"debug": (
|
|
37
|
+
"pythinker_code.cli.debug",
|
|
38
|
+
"cli",
|
|
39
|
+
"Failure/log root-cause analysis (delegates to pythinker-review).",
|
|
40
|
+
),
|
|
21
41
|
"update": (
|
|
22
42
|
"pythinker_code.cli.update",
|
|
23
43
|
"cli",
|
|
@@ -31,6 +51,10 @@ class LazySubcommandGroup(typer.core.TyperGroup):
|
|
|
31
51
|
"export",
|
|
32
52
|
"mcp",
|
|
33
53
|
"plugin",
|
|
54
|
+
"review",
|
|
55
|
+
"secscan",
|
|
56
|
+
"security-scan",
|
|
57
|
+
"debug",
|
|
34
58
|
"update",
|
|
35
59
|
"vis",
|
|
36
60
|
"web",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""`pythinker debug` — active-model wrapper for pythinker-debug."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pythinker_review.cli import debug as upstream_debug
|
|
6
|
+
|
|
7
|
+
# Importing review installs the active-model resolver used by debug.
|
|
8
|
+
from pythinker_code.cli import review as review_wrapper
|
|
9
|
+
|
|
10
|
+
_REVIEW_CLI = review_wrapper.cli
|
|
11
|
+
cli = upstream_debug.app
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""`pythinker review` — delegates to pythinker-review with active-model wiring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from pydantic import SecretStr
|
|
10
|
+
from pythinker_review.cli import review as upstream_review
|
|
11
|
+
from pythinker_review.llm.protocol import ReviewLLM
|
|
12
|
+
|
|
13
|
+
cli = upstream_review.app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class PythinkerActiveLLM:
|
|
18
|
+
"""Bridge pythinker-core's configured provider to the ReviewLLM protocol."""
|
|
19
|
+
|
|
20
|
+
chat_provider: Any
|
|
21
|
+
model_display_name: str
|
|
22
|
+
|
|
23
|
+
async def complete_json(self, *, system: str, user: str, timeout_s: float) -> str:
|
|
24
|
+
from pythinker_core import generate
|
|
25
|
+
from pythinker_core.message import Message
|
|
26
|
+
from pythinker_core.tooling.empty import EmptyToolset
|
|
27
|
+
|
|
28
|
+
result = await asyncio.wait_for(
|
|
29
|
+
generate(
|
|
30
|
+
self.chat_provider,
|
|
31
|
+
system,
|
|
32
|
+
EmptyToolset().tools,
|
|
33
|
+
[Message(role="user", content=user)],
|
|
34
|
+
),
|
|
35
|
+
timeout=timeout_s,
|
|
36
|
+
)
|
|
37
|
+
return result.message.extract_text()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_active_llm(*, model_name: str | None = None) -> ReviewLLM | None:
|
|
41
|
+
"""Build a ReviewLLM from the current Pythinker config, returning None when unset."""
|
|
42
|
+
from pythinker_code.auth.oauth import OAuthManager
|
|
43
|
+
from pythinker_code.config import LLMModel, LLMProvider, load_config
|
|
44
|
+
from pythinker_code.llm import augment_provider_with_env_vars, create_llm, model_display_name
|
|
45
|
+
|
|
46
|
+
config = load_config()
|
|
47
|
+
selected = model_name or config.default_model
|
|
48
|
+
if selected and selected in config.models:
|
|
49
|
+
model = config.models[selected].model_copy(deep=True)
|
|
50
|
+
provider = config.providers[model.provider].model_copy(deep=True)
|
|
51
|
+
else:
|
|
52
|
+
model = LLMModel(provider="", model="", max_context_size=100_000)
|
|
53
|
+
provider = LLMProvider(type="pythinker", base_url="", api_key=SecretStr(""))
|
|
54
|
+
augment_provider_with_env_vars(provider, model, provider_key=model.provider)
|
|
55
|
+
llm = create_llm(
|
|
56
|
+
provider,
|
|
57
|
+
model,
|
|
58
|
+
thinking=config.default_thinking,
|
|
59
|
+
session_id=None,
|
|
60
|
+
oauth=OAuthManager(config),
|
|
61
|
+
)
|
|
62
|
+
if llm is None:
|
|
63
|
+
return None
|
|
64
|
+
display = model_display_name(model.model, model)
|
|
65
|
+
return PythinkerActiveLLM(
|
|
66
|
+
chat_provider=cast(Any, llm.chat_provider), model_display_name=display
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_llm_with_active_model() -> ReviewLLM | None:
|
|
71
|
+
return build_active_llm()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
upstream_review.set_llm_resolver(_resolve_llm_with_active_model)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""`pythinker secscan` — active-model wrapper for pythinker-secscan."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pythinker_review.cli import secscan as upstream_secscan
|
|
6
|
+
|
|
7
|
+
# Importing review installs the active-model resolver used by secscan.
|
|
8
|
+
from pythinker_code.cli import review as review_wrapper
|
|
9
|
+
|
|
10
|
+
_REVIEW_CLI = review_wrapper.cli
|
|
11
|
+
cli = upstream_secscan.app
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""`pythinker security-scan` — active-model wrapper for Python-native Pythinker Security Scan."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pythinker_review.cli import security_scan as upstream_security_scan
|
|
9
|
+
from pythinker_review.llm.fake import FakeReviewLLM
|
|
10
|
+
from pythinker_review.llm.protocol import ReviewLLM
|
|
11
|
+
|
|
12
|
+
# Importing review installs the active-model resolver shared by review/secscan.
|
|
13
|
+
from pythinker_code.cli import review as review_wrapper
|
|
14
|
+
|
|
15
|
+
_REVIEW_CLI = review_wrapper.cli
|
|
16
|
+
cli = upstream_security_scan.app
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_security_scan_llm() -> ReviewLLM:
|
|
20
|
+
adapter = review_wrapper.build_active_llm()
|
|
21
|
+
if adapter is not None:
|
|
22
|
+
return adapter
|
|
23
|
+
fake = os.environ.get("PYTHINKER_REVIEW_FAKE_LLM_RESPONSES")
|
|
24
|
+
if fake is not None:
|
|
25
|
+
return FakeReviewLLM(scripted=fake.split("\0") if fake else ["[]"])
|
|
26
|
+
typer.secho(
|
|
27
|
+
"No active model configured. Set PYTHINKER_REVIEW_FAKE_LLM_RESPONSES for tests, "
|
|
28
|
+
"or configure a Pythinker model before running `pythinker security-scan`.",
|
|
29
|
+
fg=typer.colors.RED,
|
|
30
|
+
err=True,
|
|
31
|
+
)
|
|
32
|
+
raise typer.Exit(code=3)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
upstream_security_scan.__dict__["_resolve_llm"] = _resolve_security_scan_llm
|