codebuff-agent 2.1.74__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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- jupyter_ext/tools/user_tools.py +132 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
"""codex 모드의 슬래시 명령 (이슈 #121).
|
|
2
|
+
|
|
3
|
+
codex CLI 의 `/` 명령 중 **SaaS 형태에서만 뜻이 있는 것**(로그인·사용량·피드백
|
|
4
|
+
업로드·플러그인·앱·IDE 연동)을 뺀 나머지를 codex app-server RPC 에 잇는다.
|
|
5
|
+
`CODEX_COMMANDS` 가 자동완성과 디스패치의 단일 정본이다 — 두 곳에 목록을 두면
|
|
6
|
+
한쪽에만 명령이 생겨 "보이는데 안 되는" 명령이 조용히 생긴다.
|
|
7
|
+
|
|
8
|
+
턴을 여는 명령(`/compact`·`/review`)과 여는 척만 하는 명령(`/init` — 프롬프트로
|
|
9
|
+
바뀌어 보통 턴이 된다)을 빼면 나머지는 **턴을 열지 않는다.** 토큰을 쓰지 않고,
|
|
10
|
+
거부 응답(`-32601` 등)이 와도 세션을 닫지 않는다.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from . import codex_hooks, codex_threads
|
|
19
|
+
from .codex_bridge import (
|
|
20
|
+
CodexError,
|
|
21
|
+
CodexRejected,
|
|
22
|
+
CodexSession,
|
|
23
|
+
SseEvent,
|
|
24
|
+
_mask,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _entry(cmd_id: str, description: str, *, only_start: bool = True) -> Dict[str, Any]:
|
|
29
|
+
# 모양은 `ContextAutocompleteHandler` 의 providers 항목과 같다. 인자를 받아도
|
|
30
|
+
# `requires_arg` 는 False — True 면 라벨에 콜론이 붙는다(/review 와 같은 정책).
|
|
31
|
+
return {
|
|
32
|
+
"id": cmd_id,
|
|
33
|
+
"description": description,
|
|
34
|
+
"requires_arg": False,
|
|
35
|
+
"only_start": only_start,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
CODEX_COMMANDS: Dict[str, Dict[str, Any]] = {
|
|
40
|
+
c["id"]: c
|
|
41
|
+
for c in (
|
|
42
|
+
_entry("/new", "새 codex 스레드로 시작 (/reset 과 같음)"),
|
|
43
|
+
_entry("/compact", "지금까지의 대화를 요약해 컨텍스트를 줄임"),
|
|
44
|
+
_entry(
|
|
45
|
+
"/review",
|
|
46
|
+
"codex 코드리뷰. 예: /review, /review base main, "
|
|
47
|
+
"/review commit <sha>, /review <지시문>",
|
|
48
|
+
),
|
|
49
|
+
_entry("/model", "모델 목록·변경. 예: /model, /model <id> [effort]"),
|
|
50
|
+
_entry("/status", "현재 codex 세션의 모델·스레드·권한"),
|
|
51
|
+
_entry("/mcp", "codex 에 연결된 MCP 서버 상태"),
|
|
52
|
+
_entry("/skills", "이 작업공간에서 codex 가 쓰는 스킬 목록"),
|
|
53
|
+
_entry("/hooks", "이 작업공간에서 codex 가 쓰는 훅 목록"),
|
|
54
|
+
_entry("/diff", "작업공간의 git 변경 (pod 에서 실행)"),
|
|
55
|
+
_entry("/init", "작업공간을 살펴 AGENTS.md 지시문 초안을 만듦"),
|
|
56
|
+
_entry("/resume", "내가 연 codex 스레드를 골라 이어 붙임"),
|
|
57
|
+
_entry("/fork", "지금 대화를 문맥째 새 스레드로 갈라 이어 감"),
|
|
58
|
+
_entry("/side", "곁가지 대화 — 문맥째 갈라 묻고 /resume 으로 돌아옴"),
|
|
59
|
+
_entry("/rename", "지금 스레드 이름 바꾸기. 예: /rename 결제 리팩터링"),
|
|
60
|
+
_entry("/archive", "지금 스레드를 보관하고 새 스레드로 시작"),
|
|
61
|
+
_entry("/delete", "지금 스레드를 지움 — /delete confirm 으로 실행"),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#: `/compact` 가 끝나기를 기다리는 한도. 압축은 모델을 한 번 부르는 턴이다.
|
|
66
|
+
COMPACT_DEADLINE_S = 180.0
|
|
67
|
+
|
|
68
|
+
_INIT_PROMPT = (
|
|
69
|
+
"이 작업공간을 살펴보고, 앞으로 이 프로젝트에서 일할 에이전트가 따라야 할 규칙을 "
|
|
70
|
+
"작업공간 루트의 `AGENTS.md` 에 써 줘. 빌드·테스트 명령, 디렉터리 구조, 코드 "
|
|
71
|
+
"스타일, 하지 말아야 할 것을 짧게 정리한다. 이미 `AGENTS.md` 가 있으면 먼저 "
|
|
72
|
+
"읽고 빠진 것만 보탠다. 파일은 도구로 읽고 쓴다."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse(text: str) -> Optional[Tuple[str, str]]:
|
|
77
|
+
"""`/<명령> <인자>` 를 (명령, 인자) 로. codex 명령이 아니면 None."""
|
|
78
|
+
stripped = (text or "").strip()
|
|
79
|
+
if not stripped.startswith("/"):
|
|
80
|
+
return None
|
|
81
|
+
# 프론트(`codexCommandName`)와 같이 어떤 공백이든 이름을 끝낸다 — 줄바꿈 뒤
|
|
82
|
+
# 인자가 이름에 붙으면 명령이 아니라 보통 턴으로 새어 나간다
|
|
83
|
+
name, *rest = stripped.split(None, 1)
|
|
84
|
+
if name not in CODEX_COMMANDS:
|
|
85
|
+
return None
|
|
86
|
+
return name, (rest[0] if rest else "").strip()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def prompt_for(name: str, arg: str) -> Optional[str]:
|
|
90
|
+
"""보통 턴으로 바뀌는 명령이면 그 프롬프트. `/init` 만 그렇다."""
|
|
91
|
+
if name == "/init":
|
|
92
|
+
return f"{_INIT_PROMPT}\n\n{arg}".strip() if arg else _INIT_PROMPT
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- 디스패치 ---
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _say(text: str) -> SseEvent:
|
|
100
|
+
return (None, {"content": text})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fail(text: str) -> SseEvent:
|
|
104
|
+
return (None, {"error": text})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def dispatch(
|
|
108
|
+
session: CodexSession, name: str, arg: str
|
|
109
|
+
) -> AsyncIterator[SseEvent]:
|
|
110
|
+
"""슬래시 명령 하나를 실행한다. 항상 종료 이벤트로 끝난다."""
|
|
111
|
+
turn = _TURN_COMMANDS.get(name)
|
|
112
|
+
if turn is not None:
|
|
113
|
+
# 턴을 여는 명령은 세션이 종료 이벤트까지 낸다. `/review` 는 턴 전에 git 으로
|
|
114
|
+
# diff 를 만드는데, 그동안에도 `busy` 여야 다른 요청이 같은 소켓을 읽지 않는다
|
|
115
|
+
with session.occupied():
|
|
116
|
+
async for event in turn(session, arg):
|
|
117
|
+
yield event
|
|
118
|
+
return
|
|
119
|
+
local = _LOCAL_COMMANDS[name]
|
|
120
|
+
with session.occupied():
|
|
121
|
+
try:
|
|
122
|
+
async for event in local(session, arg):
|
|
123
|
+
yield event
|
|
124
|
+
except CodexRejected as e:
|
|
125
|
+
yield _fail(str(e))
|
|
126
|
+
except CodexError as e:
|
|
127
|
+
# 연결·핸드셰이크 실패 — 세션은 이미 쓸 수 없다
|
|
128
|
+
await session.close()
|
|
129
|
+
yield _fail(str(e))
|
|
130
|
+
yield ("complete", {"thread_id": session.thread_id})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _status(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
134
|
+
info = session.thread_info
|
|
135
|
+
lines = ["**codex 세션**", ""]
|
|
136
|
+
if not session.started:
|
|
137
|
+
lines.append("- 스레드: 아직 없음 (첫 메시지나 스레드가 필요한 명령에서 열림)")
|
|
138
|
+
else:
|
|
139
|
+
thread = info.get("thread") or {}
|
|
140
|
+
lines.append(f"- 스레드: `{session.codex_thread_id}`")
|
|
141
|
+
if thread.get("name"):
|
|
142
|
+
lines.append(f"- 이름: {thread['name']}")
|
|
143
|
+
lines.append(f"- 모델: {info.get('model') or '?'}")
|
|
144
|
+
if info.get("reasoningEffort"):
|
|
145
|
+
lines.append(f"- effort: {info['reasoningEffort']}")
|
|
146
|
+
if session.model_override:
|
|
147
|
+
effort = (
|
|
148
|
+
f" · effort {session.effort_override}" if session.effort_override else ""
|
|
149
|
+
)
|
|
150
|
+
lines.append(
|
|
151
|
+
f"- 다음 턴 모델: {session.model_override}{effort} (/model 로 고름)"
|
|
152
|
+
)
|
|
153
|
+
lines.append(f"- 권한 모드: {session.permission_mode}")
|
|
154
|
+
lines.append(f"- 작업공간: `{session.workspace_root}`")
|
|
155
|
+
yield _say("\n".join(lines))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def _model(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
159
|
+
await session.ensure_open()
|
|
160
|
+
models = (await session.call("model/list", {})).get("data") or []
|
|
161
|
+
if not arg:
|
|
162
|
+
lines = ["**codex 모델**", ""]
|
|
163
|
+
for m in models:
|
|
164
|
+
if m.get("hidden"):
|
|
165
|
+
continue
|
|
166
|
+
marks = []
|
|
167
|
+
if m.get("isDefault"):
|
|
168
|
+
marks.append("기본")
|
|
169
|
+
if m.get("id") == session.model_override:
|
|
170
|
+
marks.append("선택됨")
|
|
171
|
+
efforts = ", ".join(
|
|
172
|
+
str(e.get("reasoningEffort"))
|
|
173
|
+
for e in m.get("supportedReasoningEfforts") or []
|
|
174
|
+
)
|
|
175
|
+
mark = f" ({', '.join(marks)})" if marks else ""
|
|
176
|
+
lines.append(
|
|
177
|
+
f"- `{m.get('id')}`{mark} — {m.get('displayName') or ''}"
|
|
178
|
+
+ (f" · effort: {efforts}" if efforts else "")
|
|
179
|
+
)
|
|
180
|
+
lines += ["", "바꾸려면 `/model <id> [effort]`"]
|
|
181
|
+
yield _say("\n".join(lines))
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
parts = arg.split()
|
|
185
|
+
model_id = parts[0]
|
|
186
|
+
effort = parts[1] if len(parts) > 1 else None
|
|
187
|
+
found = next((m for m in models if m.get("id") == model_id), None)
|
|
188
|
+
if found is None:
|
|
189
|
+
yield _fail(
|
|
190
|
+
f"모델 목록에 없는 id 입니다: {model_id} — `/model` 로 목록을 봅니다."
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
supported = [
|
|
194
|
+
str(e.get("reasoningEffort"))
|
|
195
|
+
for e in found.get("supportedReasoningEfforts") or []
|
|
196
|
+
]
|
|
197
|
+
if effort is not None and effort not in supported:
|
|
198
|
+
yield _fail(
|
|
199
|
+
f"{model_id} 가 지원하지 않는 effort 입니다: {effort} (가능: {', '.join(supported)})"
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
session.set_model(model_id, effort)
|
|
203
|
+
suffix = f" · effort {effort}" if effort else ""
|
|
204
|
+
yield _say(f"다음 턴부터 `{model_id}`{suffix} 로 답합니다.")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def _mcp(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
208
|
+
await session.ensure_open()
|
|
209
|
+
result = await session.call("mcpServerStatus/list", {})
|
|
210
|
+
servers = result.get("data") or []
|
|
211
|
+
if not servers:
|
|
212
|
+
yield _say("codex 에 연결된 MCP 서버가 없습니다.")
|
|
213
|
+
return
|
|
214
|
+
lines = ["**codex MCP 서버**", ""]
|
|
215
|
+
for srv in servers:
|
|
216
|
+
tools = srv.get("tools") or {}
|
|
217
|
+
state = srv.get("runtimeStatus") or "?"
|
|
218
|
+
lines.append(
|
|
219
|
+
f"- `{srv.get('name')}` — {state} · 도구 {len(tools)} · 인증 {srv.get('authStatus') or '?'}"
|
|
220
|
+
)
|
|
221
|
+
if srv.get("toolsError"):
|
|
222
|
+
lines.append(f" - 도구 목록 오류: {srv['toolsError']}")
|
|
223
|
+
if result.get("nextCursor"):
|
|
224
|
+
lines.append("- (더 있음)")
|
|
225
|
+
yield _say("\n".join(lines))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def _skills(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
229
|
+
await session.ensure_open()
|
|
230
|
+
entries = (
|
|
231
|
+
await session.call("skills/list", {"cwds": [session.workspace_root]})
|
|
232
|
+
).get("data") or []
|
|
233
|
+
skills = [s for e in entries for s in e.get("skills") or []]
|
|
234
|
+
errs = [x for e in entries for x in e.get("errors") or []]
|
|
235
|
+
lines = []
|
|
236
|
+
if not skills:
|
|
237
|
+
lines.append("이 작업공간에서 codex 가 쓰는 스킬이 없습니다.")
|
|
238
|
+
else:
|
|
239
|
+
lines += ["**codex 스킬**", ""]
|
|
240
|
+
for s in skills:
|
|
241
|
+
off = "" if s.get("enabled", True) else " (꺼짐)"
|
|
242
|
+
desc = s.get("shortDescription") or s.get("description") or ""
|
|
243
|
+
lines.append(f"- `{s.get('name')}`{off} [{s.get('scope')}] — {desc}")
|
|
244
|
+
for x in errs:
|
|
245
|
+
lines.append(f"- 읽기 오류 `{x.get('path')}`: {x.get('message')}")
|
|
246
|
+
yield _say("\n".join(lines))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
async def _hooks(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
250
|
+
await session.ensure_open()
|
|
251
|
+
entries = (
|
|
252
|
+
await session.call("hooks/list", {"cwds": [session.workspace_root]})
|
|
253
|
+
).get("data") or []
|
|
254
|
+
hooks = [h for e in entries for h in e.get("hooks") or []]
|
|
255
|
+
notes = [
|
|
256
|
+
x for e in entries for x in (e.get("errors") or []) + (e.get("warnings") or [])
|
|
257
|
+
]
|
|
258
|
+
lines = []
|
|
259
|
+
if not hooks:
|
|
260
|
+
lines.append("이 작업공간에서 codex 가 쓰는 훅이 없습니다.")
|
|
261
|
+
else:
|
|
262
|
+
lines += ["**codex 훅**", ""]
|
|
263
|
+
for h in hooks:
|
|
264
|
+
off = "" if h.get("enabled", True) else " (꺼짐)"
|
|
265
|
+
lines.append(
|
|
266
|
+
f"- {h.get('eventName')}{off} — `{h.get('sourcePath')}` [{h.get('trustStatus')}]"
|
|
267
|
+
)
|
|
268
|
+
for x in notes:
|
|
269
|
+
lines.append(f"- {x.get('message') if isinstance(x, dict) else x}")
|
|
270
|
+
lines += ["", *_pod_hook_lines(session)]
|
|
271
|
+
yield _say("\n".join(lines))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _pod_hook_lines(session: CodexSession) -> List[str]:
|
|
275
|
+
"""pod 가 대신 돌리는 hook 목록 (이슈 #145, `codex_hooks`)."""
|
|
276
|
+
errors: List[str] = []
|
|
277
|
+
hooks = codex_hooks.discover(session.workspace_root, errors)
|
|
278
|
+
ask = session.permission_mode == "ask"
|
|
279
|
+
lines = ["**pod 가 도는 훅** (PostToolUse — 파일 수정·셸은 pod 에서 일어난다)", ""]
|
|
280
|
+
for h in hooks:
|
|
281
|
+
label = {"builtin": "내장", "repo": "저장소", "user": "사용자"}[h.scope]
|
|
282
|
+
where = h.source if h.scope == "builtin" else f"`{h.source}`"
|
|
283
|
+
skip = " — ask 모드라 건너뜀" if ask and h.scope == "repo" else ""
|
|
284
|
+
lines.append(f"- [{label}] matcher `{h.matcher or '*'}` — {where}{skip}")
|
|
285
|
+
lines += [f"- {e}" for e in errors]
|
|
286
|
+
return lines
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
#: `/diff` 가 화면에 싣는 diff 상한 — 넘으면 줄 경계에서 자르고 잘랐다고 적는다.
|
|
290
|
+
_DIFF_MAX_CHARS = 60000
|
|
291
|
+
|
|
292
|
+
#: `/review` 가 codex 에 보내는 diff 상한. `_READ_FILE_MAX_CHARS`(64,000) 와 같은 급이다
|
|
293
|
+
#: — 리뷰 모드에는 도구가 없어서, 잘린 뒷부분은 모델이 따로 읽을 길이 없다.
|
|
294
|
+
REVIEW_DIFF_MAX_CHARS = 60000
|
|
295
|
+
|
|
296
|
+
_GIT_TIMEOUT_S = 30.0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class _GitError(Exception):
|
|
300
|
+
"""git 을 못 돌렸거나 사람이 고칠 입력(없는 ref 등)."""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
async def _git(root: str, *args: str, ok: Tuple[int, ...] = (0,)) -> str:
|
|
304
|
+
"""git 을 argv 로 실행한다 — 셸을 거치지 않고 stderr 는 따로 받는다.
|
|
305
|
+
|
|
306
|
+
`_stream_command`(execute_command 도구)를 쓰지 않는 이유: 셸 문자열이라 사용자가
|
|
307
|
+
준 ref 가 셸·옵션으로 해석될 수 있고, 출력 가운데를 잘라 diff 가 깨진다.
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
proc = await asyncio.create_subprocess_exec(
|
|
311
|
+
"git",
|
|
312
|
+
"-C",
|
|
313
|
+
root,
|
|
314
|
+
*args,
|
|
315
|
+
stdout=asyncio.subprocess.PIPE,
|
|
316
|
+
stderr=asyncio.subprocess.PIPE,
|
|
317
|
+
)
|
|
318
|
+
except OSError as e:
|
|
319
|
+
raise _GitError(f"git 을 실행하지 못했습니다: {e}") from e
|
|
320
|
+
try:
|
|
321
|
+
out, err = await asyncio.wait_for(proc.communicate(), _GIT_TIMEOUT_S)
|
|
322
|
+
except asyncio.TimeoutError:
|
|
323
|
+
proc.kill()
|
|
324
|
+
await proc.wait()
|
|
325
|
+
raise _GitError(f"git 이 {_GIT_TIMEOUT_S:.0f}초 안에 끝나지 않았습니다.")
|
|
326
|
+
if proc.returncode not in ok:
|
|
327
|
+
raise _GitError(
|
|
328
|
+
_first_line(err.decode("utf-8", "replace"))
|
|
329
|
+
or f"git 이 실패했습니다 (rc={proc.returncode})."
|
|
330
|
+
)
|
|
331
|
+
return out.decode("utf-8", "replace")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _cut(text: str, limit: int) -> Tuple[str, bool]:
|
|
335
|
+
"""`limit` 자 안에서 마지막 줄바꿈까지만 남긴다. 줄 가운데를 자르면 diff 가 깨진다."""
|
|
336
|
+
if len(text) <= limit:
|
|
337
|
+
return text, False
|
|
338
|
+
head = text[:limit]
|
|
339
|
+
return head[: max(head.rfind("\n"), 0)], True
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
async def workspace_diff(root: str) -> Tuple[str, bool]:
|
|
343
|
+
"""작업공간 git 변경을 마크다운(상태 목록 + ```diff 펜스)으로 만든다.
|
|
344
|
+
|
|
345
|
+
codex 모드 `/diff` 와 langchain 모드 `GET /codebuff/git/diff` 가 같이 쓴다 (이슈 #147).
|
|
346
|
+
두 번째 값은 git 을 읽었는지 — False 면 첫 값은 사람이 읽을 오류 문장이다.
|
|
347
|
+
"""
|
|
348
|
+
# 한글 경로가 "\355\225\234" 8진수로 나오지 않게 한다
|
|
349
|
+
quote = ("-c", "core.quotePath=false")
|
|
350
|
+
try:
|
|
351
|
+
changes = (await _git(root, *quote, "status", "--short")).rstrip()
|
|
352
|
+
except _GitError as e:
|
|
353
|
+
return f"git 변경을 읽지 못했습니다: {e}", False
|
|
354
|
+
if not changes:
|
|
355
|
+
return "작업공간에 git 변경이 없습니다.", True
|
|
356
|
+
try:
|
|
357
|
+
body = await _git(root, *quote, "diff", "HEAD", "--no-color", "--no-ext-diff")
|
|
358
|
+
except _GitError:
|
|
359
|
+
# 커밋이 하나도 없으면 HEAD 가 없다 — 스테이징된 것만 보인다
|
|
360
|
+
try:
|
|
361
|
+
body = await _git(
|
|
362
|
+
root, *quote, "diff", "--cached", "--no-color", "--no-ext-diff"
|
|
363
|
+
)
|
|
364
|
+
except _GitError:
|
|
365
|
+
body = ""
|
|
366
|
+
body, was_cut = _cut(body.rstrip(), _DIFF_MAX_CHARS)
|
|
367
|
+
parts = ["**작업공간 변경**", "", "```", changes, "```"]
|
|
368
|
+
if body:
|
|
369
|
+
parts += ["", "```diff", body, "```"]
|
|
370
|
+
if was_cut:
|
|
371
|
+
parts += ["", f"(diff 가 길어 앞 {_DIFF_MAX_CHARS:,}자 안쪽만 보입니다)"]
|
|
372
|
+
return "\n".join(parts), True
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def _diff(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
376
|
+
text, ok = await workspace_diff(session.workspace_root)
|
|
377
|
+
yield _say(text) if ok else _fail(text)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _first_line(text: Any) -> str:
|
|
381
|
+
for line in str(text or "").splitlines():
|
|
382
|
+
if line.strip():
|
|
383
|
+
return line.strip()
|
|
384
|
+
return ""
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def _commit_of(root: str, ref: str) -> str:
|
|
388
|
+
"""사용자가 준 ref 를 커밋 sha 로. `--end-of-options` 라 `--output=…` 도 ref 로 읽힌다."""
|
|
389
|
+
try:
|
|
390
|
+
return (
|
|
391
|
+
await _git(
|
|
392
|
+
root,
|
|
393
|
+
"rev-parse",
|
|
394
|
+
"--verify",
|
|
395
|
+
"--quiet",
|
|
396
|
+
"--end-of-options",
|
|
397
|
+
ref + "^{commit}",
|
|
398
|
+
)
|
|
399
|
+
).strip()
|
|
400
|
+
except _GitError:
|
|
401
|
+
raise _GitError(f"커밋을 찾지 못했습니다: {ref}")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def _uncommitted(root: str) -> str:
|
|
405
|
+
"""HEAD 대비 변경 + 추적하지 않는 파일. 추적 안 한 파일은 `/dev/null` 과 비교한다."""
|
|
406
|
+
try:
|
|
407
|
+
await _git(root, "rev-parse", "--verify", "--quiet", "HEAD")
|
|
408
|
+
except _GitError:
|
|
409
|
+
raise _GitError(
|
|
410
|
+
"커밋이 하나도 없는 저장소라 비교할 기준이 없습니다. 첫 커밋 뒤에 다시 부르세요."
|
|
411
|
+
)
|
|
412
|
+
parts = [await _git(root, "diff", "HEAD", "--no-color", "--no-ext-diff")]
|
|
413
|
+
size = len(parts[0])
|
|
414
|
+
listed = await _git(root, "ls-files", "--others", "--exclude-standard", "-z")
|
|
415
|
+
for path in filter(None, listed.split("\0")):
|
|
416
|
+
if size > REVIEW_DIFF_MAX_CHARS:
|
|
417
|
+
break # 어차피 잘릴 뒷부분을 만들려고 git 을 더 부르지 않는다
|
|
418
|
+
# 다르면 rc 1 이다
|
|
419
|
+
one = await _git(
|
|
420
|
+
root, "diff", "--no-index", "--no-color", "--", "/dev/null", path, ok=(0, 1)
|
|
421
|
+
)
|
|
422
|
+
parts.append(one)
|
|
423
|
+
size += len(one)
|
|
424
|
+
return "".join(parts)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# 기본 지시문의 관점별 지침은 셀 코드리뷰와 같은 키다 (이슈 #218). 셀 리뷰 전용 형식인
|
|
428
|
+
# `review_summary_prompt`(셀별 전체 개선 코드)는 diff 리뷰에 맞지 않아 쓰지 않는다.
|
|
429
|
+
_REVIEW_SECTION_KEYS = (
|
|
430
|
+
"review_security_prompt",
|
|
431
|
+
"review_performance_prompt",
|
|
432
|
+
"review_quality_prompt",
|
|
433
|
+
)
|
|
434
|
+
# diff 리뷰의 출력 형식 — 키의 형식 예시는 셀 번호로 되어 있다
|
|
435
|
+
_REVIEW_HEAD = (
|
|
436
|
+
"아래 변경을 코드리뷰해 줘. 버그·경계값·빠진 테스트를 포함해 심각한 것부터 짚고, "
|
|
437
|
+
"지적마다 셀 번호 대신 파일과 줄을 적어 줘. "
|
|
438
|
+
"관점별 지침은 다음과 같다."
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _review_default_focus(root: str) -> str:
|
|
443
|
+
"""인자 없는 `/review` 의 지시문 — 코드리뷰 키로 만들고 `{{RESOURCE_CONTEXT}}` 를 채운다."""
|
|
444
|
+
from codebuff_core.managers.config_manager import prompt_value
|
|
445
|
+
|
|
446
|
+
from ..http_handlers import _inject_resource_context
|
|
447
|
+
|
|
448
|
+
sections = [prompt_value(key) for key in _REVIEW_SECTION_KEYS]
|
|
449
|
+
text = (
|
|
450
|
+
"\n\n".join([_REVIEW_HEAD, *sections])
|
|
451
|
+
+ "\n"
|
|
452
|
+
+ prompt_value("review_rules_prompt")
|
|
453
|
+
)
|
|
454
|
+
return _inject_resource_context(text, {"server_root_dir": root})
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
async def _review(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
458
|
+
"""`review/start` 를 **custom 대상 + inline** 으로 보낸다.
|
|
459
|
+
|
|
460
|
+
리뷰 모드에서는 dynamic tool 이 불리지 않고(실측 0회), codex 서버는 작업공간과 다른
|
|
461
|
+
파일시스템에 있다. 그래서 `uncommittedChanges`·`baseBranch`·`commit` 대상을 codex 에
|
|
462
|
+
맡기지 않고, 재료(diff)를 pod 에서 만들어 지시문에 싣는다.
|
|
463
|
+
"""
|
|
464
|
+
root = session.workspace_root
|
|
465
|
+
word, _, rest = arg.partition(" ")
|
|
466
|
+
rest = rest.strip()
|
|
467
|
+
focus = ""
|
|
468
|
+
try:
|
|
469
|
+
if word in ("base", "commit"):
|
|
470
|
+
if not rest:
|
|
471
|
+
raise _GitError(
|
|
472
|
+
f"사용법: /review {word} <{'브랜치' if word == 'base' else 'sha'}>"
|
|
473
|
+
)
|
|
474
|
+
sha = await _commit_of(root, rest)
|
|
475
|
+
if word == "base":
|
|
476
|
+
label = f"`{rest}` 브랜치에서 갈라진 뒤의 변경 (merge-base..HEAD)"
|
|
477
|
+
body = await _git(
|
|
478
|
+
root, "diff", "--no-color", "--no-ext-diff", f"{sha}...HEAD"
|
|
479
|
+
)
|
|
480
|
+
else:
|
|
481
|
+
title = (await _git(root, "log", "-1", "--format=%s", sha)).strip()
|
|
482
|
+
label = f"커밋 `{sha[:12]}` {title}"
|
|
483
|
+
body = await _git(
|
|
484
|
+
root, "show", "--no-color", "--no-ext-diff", "--format=", sha
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
label = "커밋하지 않은 변경 (HEAD 대비, 추적하지 않는 파일 포함)"
|
|
488
|
+
body = await _uncommitted(root)
|
|
489
|
+
if arg:
|
|
490
|
+
focus = arg
|
|
491
|
+
except _GitError as e:
|
|
492
|
+
yield _fail(f"리뷰할 변경을 만들지 못했습니다: {e}")
|
|
493
|
+
yield ("complete", {"thread_id": session.thread_id})
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
body, was_cut = _cut(body.rstrip(), REVIEW_DIFF_MAX_CHARS)
|
|
497
|
+
if not focus:
|
|
498
|
+
if not body:
|
|
499
|
+
yield _say(f"리뷰할 변경이 없습니다 — {label}.")
|
|
500
|
+
yield ("complete", {"thread_id": session.thread_id})
|
|
501
|
+
return
|
|
502
|
+
focus = _review_default_focus(root)
|
|
503
|
+
|
|
504
|
+
lines = [focus, "", f"대상: {label}"]
|
|
505
|
+
if body:
|
|
506
|
+
lines += ["", "```diff", body, "```"]
|
|
507
|
+
else:
|
|
508
|
+
lines += ["", "(미커밋 변경 없음 — 위 지시만 보고 답한다)"]
|
|
509
|
+
if was_cut:
|
|
510
|
+
lines += [
|
|
511
|
+
"",
|
|
512
|
+
f"(diff 가 {REVIEW_DIFF_MAX_CHARS:,}자를 넘어 줄 경계에서 잘랐다. "
|
|
513
|
+
"뒷부분은 보지 못했다고 리뷰에 밝혀 줘.)",
|
|
514
|
+
]
|
|
515
|
+
try:
|
|
516
|
+
await session.ensure_open()
|
|
517
|
+
except CodexError as e:
|
|
518
|
+
await session.close()
|
|
519
|
+
yield _fail(str(e))
|
|
520
|
+
yield ("complete", {"thread_id": session.thread_id})
|
|
521
|
+
return
|
|
522
|
+
yield (None, {"status": "코드리뷰"})
|
|
523
|
+
async for event in session.run_request(
|
|
524
|
+
"review/start",
|
|
525
|
+
{
|
|
526
|
+
# diff·지시문이 pod 밖으로 나간다 — `run()` 의 프롬프트와 같은 관문을 지난다
|
|
527
|
+
"target": {"type": "custom", "instructions": _mask("\n".join(lines))[0]},
|
|
528
|
+
"delivery": "inline",
|
|
529
|
+
},
|
|
530
|
+
):
|
|
531
|
+
yield event
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
async def _compact(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
535
|
+
if not session.started:
|
|
536
|
+
yield _say("압축할 대화가 없습니다. 먼저 메시지를 보내 주세요.")
|
|
537
|
+
yield ("complete", {"thread_id": session.thread_id})
|
|
538
|
+
return
|
|
539
|
+
yield (None, {"status": "컨텍스트 압축"})
|
|
540
|
+
async for event in session.run_request(
|
|
541
|
+
"thread/compact/start", {}, deadline_s=COMPACT_DEADLINE_S
|
|
542
|
+
):
|
|
543
|
+
yield event
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# --- 스레드 관리 — 목록의 정본은 `codex_threads` (codex 의 thread/list 는 남의 것도 준다) ---
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _thread_line(thread: Dict[str, Any]) -> str:
|
|
550
|
+
name = thread.get("name") or _first_line(thread.get("preview")) or "(이름 없음)"
|
|
551
|
+
return f"`{thread.get('id')}` {name}"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def _resume(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
555
|
+
if arg:
|
|
556
|
+
if codex_threads.find(arg) is None:
|
|
557
|
+
yield _fail(
|
|
558
|
+
f"이 사용자가 연 스레드가 아닙니다: {arg} — `/resume` 으로 목록을 봅니다."
|
|
559
|
+
)
|
|
560
|
+
return
|
|
561
|
+
result = await session.switch_thread(arg)
|
|
562
|
+
thread = result.get("thread") or {}
|
|
563
|
+
yield _say(f"스레드 {_thread_line(thread)} 에 이어 붙었습니다.")
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
entries = [t for t in codex_threads.load() if not t.get("archived")]
|
|
567
|
+
entries = entries[: codex_threads.LIST_LIMIT]
|
|
568
|
+
if not entries:
|
|
569
|
+
yield _say("이어 붙일 codex 스레드가 없습니다. 첫 메시지를 보내면 생깁니다.")
|
|
570
|
+
return
|
|
571
|
+
await session.ensure_connected()
|
|
572
|
+
rows = []
|
|
573
|
+
for entry in entries:
|
|
574
|
+
row: Dict[str, Any] = {
|
|
575
|
+
"id": entry["id"],
|
|
576
|
+
"kind": entry.get("kind") or "start",
|
|
577
|
+
"createdAt": entry.get("createdAt"),
|
|
578
|
+
"current": entry["id"] == session.codex_thread_id,
|
|
579
|
+
}
|
|
580
|
+
try:
|
|
581
|
+
thread = (
|
|
582
|
+
await session.call(
|
|
583
|
+
"thread/read", {"threadId": entry["id"], "includeTurns": False}
|
|
584
|
+
)
|
|
585
|
+
).get("thread") or {}
|
|
586
|
+
except CodexRejected as e:
|
|
587
|
+
# 서버에서 지워졌거나 아직 기록이 없는 스레드 — 목록에는 두고 고르지 못하게 한다
|
|
588
|
+
row["error"] = str(e)
|
|
589
|
+
else:
|
|
590
|
+
row["name"] = thread.get("name") or ""
|
|
591
|
+
row["preview"] = _first_line(thread.get("preview"))
|
|
592
|
+
row["updatedAt"] = thread.get("updatedAt")
|
|
593
|
+
rows.append(row)
|
|
594
|
+
yield ("codex_threads", {"threads": rows})
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
async def _fork(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
598
|
+
async for event in _branch(session, "fork"):
|
|
599
|
+
yield event
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
async def _side(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
603
|
+
async for event in _branch(session, "side"):
|
|
604
|
+
yield event
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
async def _branch(session: CodexSession, kind: str) -> AsyncIterator[SseEvent]:
|
|
608
|
+
if not session.started:
|
|
609
|
+
yield _say("가를 대화가 없습니다. 먼저 메시지를 보내 주세요.")
|
|
610
|
+
return
|
|
611
|
+
parent = session.codex_thread_id
|
|
612
|
+
result = await session.fork_thread(kind)
|
|
613
|
+
new_id = (result.get("thread") or {}).get("id")
|
|
614
|
+
if kind == "side":
|
|
615
|
+
yield _say(
|
|
616
|
+
f"곁가지 스레드 `{new_id}` 로 옮겼습니다. 지금까지의 문맥을 그대로 봅니다. "
|
|
617
|
+
f"끝나면 `/resume {parent}` 로 원래 스레드에 돌아갑니다."
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
yield _say(
|
|
621
|
+
f"스레드를 갈랐습니다: 새 스레드 `{new_id}` 에서 이어 갑니다. "
|
|
622
|
+
f"원래 스레드는 `/resume {parent}` 로 돌아갈 수 있습니다."
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
async def _rename(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
627
|
+
if not arg:
|
|
628
|
+
yield _fail("사용법: /rename <이름>")
|
|
629
|
+
return
|
|
630
|
+
if not session.started:
|
|
631
|
+
yield _say("이름을 붙일 스레드가 없습니다. 먼저 메시지를 보내 주세요.")
|
|
632
|
+
return
|
|
633
|
+
tid = session.codex_thread_id
|
|
634
|
+
await session.call("thread/name/set", {"threadId": tid, "name": _mask(arg)[0]})
|
|
635
|
+
codex_threads.update(tid, name=arg)
|
|
636
|
+
yield _say(f"스레드 `{tid}` 의 이름을 '{arg}' 로 바꿨습니다.")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
async def _archive(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
640
|
+
if not session.started:
|
|
641
|
+
yield _say("보관할 스레드가 없습니다.")
|
|
642
|
+
return
|
|
643
|
+
tid = session.codex_thread_id
|
|
644
|
+
await session.call("thread/archive", {"threadId": tid})
|
|
645
|
+
codex_threads.update(tid, archived=True)
|
|
646
|
+
session.leave_thread()
|
|
647
|
+
yield _say(f"스레드 `{tid}` 를 보관했습니다. 다음 메시지는 새 스레드로 시작합니다.")
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
async def _delete(session: CodexSession, arg: str) -> AsyncIterator[SseEvent]:
|
|
651
|
+
if not session.started:
|
|
652
|
+
yield _say("지울 스레드가 없습니다.")
|
|
653
|
+
return
|
|
654
|
+
tid = session.codex_thread_id
|
|
655
|
+
if arg != "confirm":
|
|
656
|
+
yield _say(
|
|
657
|
+
f"스레드 `{tid}` 를 codex 에서 지웁니다. 되돌릴 수 없습니다 — "
|
|
658
|
+
"실행하려면 `/delete confirm` 을 보냅니다."
|
|
659
|
+
)
|
|
660
|
+
return
|
|
661
|
+
await session.call("thread/delete", {"threadId": tid})
|
|
662
|
+
codex_threads.forget(tid)
|
|
663
|
+
session.leave_thread()
|
|
664
|
+
yield _say(f"스레드 `{tid}` 를 지웠습니다. 다음 메시지는 새 스레드로 시작합니다.")
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
_LOCAL_COMMANDS = {
|
|
668
|
+
"/status": _status,
|
|
669
|
+
"/model": _model,
|
|
670
|
+
"/mcp": _mcp,
|
|
671
|
+
"/skills": _skills,
|
|
672
|
+
"/hooks": _hooks,
|
|
673
|
+
"/diff": _diff,
|
|
674
|
+
"/resume": _resume,
|
|
675
|
+
"/fork": _fork,
|
|
676
|
+
"/side": _side,
|
|
677
|
+
"/rename": _rename,
|
|
678
|
+
"/archive": _archive,
|
|
679
|
+
"/delete": _delete,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
_TURN_COMMANDS = {
|
|
683
|
+
"/compact": _compact,
|
|
684
|
+
"/review": _review,
|
|
685
|
+
}
|