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,86 @@
|
|
|
1
|
+
"""프로젝트 디렉터리 자동완성 — 설정 화면의 디렉터리 칸이 부른다 (이슈 #171).
|
|
2
|
+
|
|
3
|
+
입력한 경로의 부모 디렉터리 **한 단계만** 읽는다. `@` 파일 멘션 자동완성의 `FileIndex` 는
|
|
4
|
+
요청마다 트리를 통째로 걸어서(최대 10,000 항목) 키 입력마다 부르기에는 무겁다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from codebuff_core.directory_policy import COMMON_EXCLUDED_DIRS
|
|
13
|
+
|
|
14
|
+
MAX_RESULTS = 50
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _inside(path: str, roots: List[str]) -> bool:
|
|
18
|
+
return any(
|
|
19
|
+
path == root or path.startswith(root.rstrip(os.sep) + os.sep) for root in roots
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _has_agents_md(path: str) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
return any(
|
|
26
|
+
name.lower() == "agents.md" and os.path.isfile(os.path.join(path, name))
|
|
27
|
+
for name in os.listdir(path)
|
|
28
|
+
)
|
|
29
|
+
except OSError:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def complete_directories(
|
|
34
|
+
prefix: str,
|
|
35
|
+
base: str,
|
|
36
|
+
limit: int = MAX_RESULTS,
|
|
37
|
+
roots: Optional[List[str]] = None,
|
|
38
|
+
) -> List[Dict]:
|
|
39
|
+
"""`prefix` 에 이어질 디렉터리 후보.
|
|
40
|
+
|
|
41
|
+
상대 경로는 `base` 기준이다(저장값 해석과 같은 기준). `/` 로 끝나면 그 디렉터리의
|
|
42
|
+
자식, 아니면 부모의 자식 중 마지막 조각으로 시작하는 것(대소문자 무시)을 준다.
|
|
43
|
+
부모가 허용 루트(`base`·홈) 밖이거나 없으면 빈 목록이다. 링크 너머가 허용 루트 밖인
|
|
44
|
+
후보도 뺀다 — 골라도 그 아래로는 더 못 내려간다.
|
|
45
|
+
|
|
46
|
+
`~` 는 풀지 않는다. 저장값 해석(`_get_effective_workspace_root`)과 AGENTS.md 상태 API
|
|
47
|
+
가 안 풀어서, 여기서만 풀면 후보는 뜨는데 저장한 경로는 서버 루트로 폴백된다.
|
|
48
|
+
"""
|
|
49
|
+
typed = prefix
|
|
50
|
+
if not os.path.isabs(typed):
|
|
51
|
+
typed = os.path.join(base, typed)
|
|
52
|
+
parent, fragment = os.path.split(typed)
|
|
53
|
+
if roots is None:
|
|
54
|
+
roots = [base, os.path.expanduser("~")]
|
|
55
|
+
real_roots = [os.path.realpath(r) for r in roots]
|
|
56
|
+
if not os.path.isdir(parent) or not _inside(os.path.realpath(parent), real_roots):
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
show_hidden = fragment.startswith(".")
|
|
60
|
+
needle = fragment.lower()
|
|
61
|
+
try:
|
|
62
|
+
entries = [e for e in os.scandir(parent) if e.is_dir()]
|
|
63
|
+
except OSError:
|
|
64
|
+
return []
|
|
65
|
+
names = sorted(
|
|
66
|
+
(
|
|
67
|
+
e.name
|
|
68
|
+
for e in entries
|
|
69
|
+
if e.name.lower().startswith(needle)
|
|
70
|
+
and e.name not in COMMON_EXCLUDED_DIRS
|
|
71
|
+
and (show_hidden or not e.name.startswith("."))
|
|
72
|
+
and (not e.is_symlink() or _inside(os.path.realpath(e.path), real_roots))
|
|
73
|
+
),
|
|
74
|
+
key=str.lower,
|
|
75
|
+
)[:limit]
|
|
76
|
+
result = []
|
|
77
|
+
for name in names:
|
|
78
|
+
path = os.path.normpath(os.path.join(parent, name))
|
|
79
|
+
result.append(
|
|
80
|
+
{
|
|
81
|
+
"path": path,
|
|
82
|
+
"git": os.path.exists(os.path.join(path, ".git")),
|
|
83
|
+
"agentsMd": _has_agents_md(path),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return result
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""File finder with fzf-style subsequence matching.
|
|
2
|
+
|
|
3
|
+
Provides a cached file index with both prefix and fuzzy matching,
|
|
4
|
+
shared by the @ file-mention autocomplete handler and the file_find agent tool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from codebuff_core.directory_policy import FILE_INDEX_EXCLUDED_DIRS as _EXCLUDE_DIRS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _fzf_score(query: str, candidate: str) -> float | None:
|
|
17
|
+
"""fzf-style subsequence match. Returns None if no match."""
|
|
18
|
+
q, c = query.lower(), candidate.lower()
|
|
19
|
+
qi = ci = 0
|
|
20
|
+
positions: list[int] = []
|
|
21
|
+
while qi < len(q) and ci < len(c):
|
|
22
|
+
if q[qi] == c[ci]:
|
|
23
|
+
positions.append(ci)
|
|
24
|
+
qi += 1
|
|
25
|
+
ci += 1
|
|
26
|
+
if qi < len(q):
|
|
27
|
+
return None
|
|
28
|
+
score = 100.0
|
|
29
|
+
# Consecutive character bonus
|
|
30
|
+
consecutive = sum(
|
|
31
|
+
1 for i in range(1, len(positions)) if positions[i] == positions[i - 1] + 1
|
|
32
|
+
)
|
|
33
|
+
score += consecutive * 10
|
|
34
|
+
# Start-of-string bonus
|
|
35
|
+
if positions and positions[0] == 0:
|
|
36
|
+
score += 15
|
|
37
|
+
# Gap penalty
|
|
38
|
+
total_gap = sum(
|
|
39
|
+
positions[i] - positions[i - 1] - 1
|
|
40
|
+
for i in range(1, len(positions))
|
|
41
|
+
if positions[i] > positions[i - 1] + 1
|
|
42
|
+
)
|
|
43
|
+
score -= total_gap * 3
|
|
44
|
+
# Length difference penalty
|
|
45
|
+
score -= (len(candidate) - len(query)) * 1
|
|
46
|
+
return score
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FileIndex:
|
|
50
|
+
"""Caches a directory tree in memory and provides fuzzy search."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
base_dir: str | Path,
|
|
55
|
+
ttl: int = 30,
|
|
56
|
+
max_files: int = 10_000,
|
|
57
|
+
):
|
|
58
|
+
self._base_dir = Path(base_dir).resolve()
|
|
59
|
+
self._ttl = ttl
|
|
60
|
+
self._max_files = max_files
|
|
61
|
+
self._entries: list[str] = [] # relative paths
|
|
62
|
+
self._built_at: float = 0.0
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Index management
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _maybe_rebuild(self) -> None:
|
|
69
|
+
if time.monotonic() - self._built_at < self._ttl and self._entries:
|
|
70
|
+
return
|
|
71
|
+
self._build_index()
|
|
72
|
+
|
|
73
|
+
def _build_index(self) -> None:
|
|
74
|
+
entries: list[str] = []
|
|
75
|
+
base = str(self._base_dir)
|
|
76
|
+
for dirpath, dirnames, filenames in os.walk(base):
|
|
77
|
+
# Prune excluded directories in-place
|
|
78
|
+
dirnames[:] = [
|
|
79
|
+
d for d in dirnames if d not in _EXCLUDE_DIRS and not d.startswith(".")
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
rel_dir = os.path.relpath(dirpath, base)
|
|
83
|
+
if rel_dir == ".":
|
|
84
|
+
rel_dir = ""
|
|
85
|
+
|
|
86
|
+
# Add directory entry (with trailing /)
|
|
87
|
+
if rel_dir:
|
|
88
|
+
entries.append(rel_dir + "/")
|
|
89
|
+
|
|
90
|
+
for fname in filenames:
|
|
91
|
+
if fname.startswith("."):
|
|
92
|
+
continue
|
|
93
|
+
if rel_dir:
|
|
94
|
+
entries.append(os.path.join(rel_dir, fname))
|
|
95
|
+
else:
|
|
96
|
+
entries.append(fname)
|
|
97
|
+
|
|
98
|
+
if len(entries) >= self._max_files:
|
|
99
|
+
break
|
|
100
|
+
if len(entries) >= self._max_files:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
self._entries = entries
|
|
104
|
+
self._built_at = time.monotonic()
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
# Search methods
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def fuzzy_search(
|
|
111
|
+
self,
|
|
112
|
+
query: str,
|
|
113
|
+
max_results: int = 20,
|
|
114
|
+
supported_exts: set[str] | None = None,
|
|
115
|
+
include_dirs: bool = True,
|
|
116
|
+
) -> list[dict]:
|
|
117
|
+
"""Find files by fuzzy matching on the query string.
|
|
118
|
+
|
|
119
|
+
If query contains '/', the part before the last '/' is used as a
|
|
120
|
+
directory filter and only the basename portion is fuzzy-matched.
|
|
121
|
+
|
|
122
|
+
Returns list of dicts: [{path, score, type}, ...]
|
|
123
|
+
"""
|
|
124
|
+
self._maybe_rebuild()
|
|
125
|
+
if not query:
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
# Split directory filter from basename query
|
|
129
|
+
dir_filter = ""
|
|
130
|
+
basename_query = query
|
|
131
|
+
if "/" in query:
|
|
132
|
+
last_slash = query.rfind("/")
|
|
133
|
+
dir_filter = query[:last_slash]
|
|
134
|
+
basename_query = query[last_slash + 1 :]
|
|
135
|
+
if not basename_query:
|
|
136
|
+
# query like "src/" → fall back to prefix search
|
|
137
|
+
return self.prefix_search(query, max_results, supported_exts)
|
|
138
|
+
|
|
139
|
+
# Filter candidates
|
|
140
|
+
candidates: list[str] = []
|
|
141
|
+
for entry in self._entries:
|
|
142
|
+
is_dir = entry.endswith("/")
|
|
143
|
+
|
|
144
|
+
if not include_dirs and is_dir:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if not is_dir and supported_exts is not None:
|
|
148
|
+
ext = os.path.splitext(entry)[1]
|
|
149
|
+
if ext not in supported_exts:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if dir_filter and not entry.startswith(dir_filter):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
candidates.append(entry)
|
|
156
|
+
|
|
157
|
+
if not candidates:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
# fzf-style subsequence matching on basename
|
|
161
|
+
scored: list[tuple[str, float]] = []
|
|
162
|
+
for entry in candidates:
|
|
163
|
+
basename = os.path.basename(entry.rstrip("/"))
|
|
164
|
+
s = _fzf_score(basename_query, basename)
|
|
165
|
+
if s is None:
|
|
166
|
+
continue
|
|
167
|
+
# Depth penalty: shallower files preferred
|
|
168
|
+
depth = entry.count("/") - (1 if entry.endswith("/") else 0)
|
|
169
|
+
s -= depth * 1.0
|
|
170
|
+
scored.append((entry, s))
|
|
171
|
+
|
|
172
|
+
# Sort by score descending
|
|
173
|
+
scored.sort(key=lambda x: -x[1])
|
|
174
|
+
|
|
175
|
+
results: list[dict] = []
|
|
176
|
+
for entry, score in scored[:max_results]:
|
|
177
|
+
is_dir = entry.endswith("/")
|
|
178
|
+
results.append(
|
|
179
|
+
{
|
|
180
|
+
"path": entry.rstrip("/") if is_dir else entry,
|
|
181
|
+
"score": round(score, 1),
|
|
182
|
+
"type": "directory" if is_dir else "file",
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
def prefix_search(
|
|
188
|
+
self,
|
|
189
|
+
prefix: str,
|
|
190
|
+
max_results: int = 20,
|
|
191
|
+
supported_exts: set[str] | None = None,
|
|
192
|
+
) -> list[dict]:
|
|
193
|
+
"""Find files/dirs matching a path prefix (existing glob behavior)."""
|
|
194
|
+
self._maybe_rebuild()
|
|
195
|
+
|
|
196
|
+
# Normalize: ensure no trailing slash for matching (unless root)
|
|
197
|
+
norm_prefix = prefix.rstrip("/") if prefix != "/" else ""
|
|
198
|
+
results: list[dict] = []
|
|
199
|
+
|
|
200
|
+
for entry in self._entries:
|
|
201
|
+
is_dir = entry.endswith("/")
|
|
202
|
+
entry_clean = entry.rstrip("/")
|
|
203
|
+
|
|
204
|
+
# Match: entry starts with prefix
|
|
205
|
+
if not entry_clean.startswith(norm_prefix):
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Only direct children if prefix looks like a directory
|
|
209
|
+
if prefix.endswith("/") or not prefix:
|
|
210
|
+
remainder = entry_clean[len(norm_prefix) :].lstrip("/")
|
|
211
|
+
if "/" in remainder:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if not is_dir and supported_exts is not None:
|
|
215
|
+
ext = os.path.splitext(entry)[1]
|
|
216
|
+
if ext not in supported_exts:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
results.append(
|
|
220
|
+
{
|
|
221
|
+
"path": entry_clean,
|
|
222
|
+
"score": 100.0,
|
|
223
|
+
"type": "directory" if is_dir else "file",
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
if len(results) >= max_results:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
# Sort: directories first, then alphabetical
|
|
230
|
+
results.sort(key=lambda r: (r["type"] != "directory", r["path"].lower()))
|
|
231
|
+
return results
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ----------------------------------------------------------------------
|
|
235
|
+
# Navigate File (IntelliJ 식 파일 이름 검색)
|
|
236
|
+
# ----------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
# 키 입력마다 요청이 오므로 인덱스를 루트별로 한 벌만 두고 TTL 로 갱신한다.
|
|
239
|
+
_SHARED_INDEXES: dict[str, FileIndex] = {}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def shared_file_index(base_dir: str | Path) -> FileIndex:
|
|
243
|
+
key = str(Path(base_dir).resolve())
|
|
244
|
+
index = _SHARED_INDEXES.get(key)
|
|
245
|
+
if index is None:
|
|
246
|
+
index = _SHARED_INDEXES[key] = FileIndex(key, max_files=50_000)
|
|
247
|
+
return index
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# 머리글자(IntelliJ 의 camel hump) 매칭 가산점. "ACT" 는 AppConfigTests 의 머리글자라
|
|
251
|
+
# 어디든 부분열로만 맞는 factory.py 보다 위에 와야 하고, 머리글자가 "act" 로 시작하는
|
|
252
|
+
# AppConfigTests 가 머리글자 a·c·s·t 사이에 흩어져 맞는 AccessControlService 보다 위다.
|
|
253
|
+
_INITIALS_PREFIX_BONUS = 80.0
|
|
254
|
+
_INITIALS_BONUS = 50.0
|
|
255
|
+
_SEPARATORS = "._-/ "
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _initials(name: str) -> str:
|
|
259
|
+
"""이름 첫 글자, 소문자 뒤 대문자, 구분자 뒤 글자를 모은다 (소문자로)."""
|
|
260
|
+
out = []
|
|
261
|
+
for i, ch in enumerate(name):
|
|
262
|
+
prev = name[i - 1] if i else ""
|
|
263
|
+
if (
|
|
264
|
+
i == 0
|
|
265
|
+
or (ch.isupper() and prev.islower())
|
|
266
|
+
or (prev in _SEPARATORS and ch not in _SEPARATORS)
|
|
267
|
+
):
|
|
268
|
+
out.append(ch.lower())
|
|
269
|
+
return "".join(out)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _is_subsequence(query: str, text: str) -> bool:
|
|
273
|
+
it = iter(text)
|
|
274
|
+
return all(ch in it for ch in query)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def navigate_files(
|
|
278
|
+
base_dir: str | Path, query: str, max_results: int = 50
|
|
279
|
+
) -> list[dict]:
|
|
280
|
+
"""파일 이름 퍼지 검색. 확장자를 거르지 않고 디렉터리는 뺀다."""
|
|
281
|
+
query = query.strip()
|
|
282
|
+
if not query:
|
|
283
|
+
return []
|
|
284
|
+
# `폴더/이름`: FileIndex 의 폴더 필터는 루트부터 맞추므로 쓰지 않고, 이름만 검색한 뒤
|
|
285
|
+
# 폴더 부분이 경로 어디에든 있는 것만 남긴다 (IntelliJ 와 같다)
|
|
286
|
+
dir_hint, _, name_query = query.rpartition("/")
|
|
287
|
+
name_query = name_query.lower()
|
|
288
|
+
if not name_query:
|
|
289
|
+
return []
|
|
290
|
+
results = shared_file_index(base_dir).fuzzy_search(
|
|
291
|
+
name_query, max_results=max(500, max_results), include_dirs=False
|
|
292
|
+
)
|
|
293
|
+
if dir_hint:
|
|
294
|
+
hint = dir_hint.strip("/").lower() + "/"
|
|
295
|
+
results = [r for r in results if hint in "/" + r["path"].lower()]
|
|
296
|
+
for r in results:
|
|
297
|
+
initials = _initials(os.path.basename(r["path"]))
|
|
298
|
+
if initials.startswith(name_query):
|
|
299
|
+
r["score"] = round(r["score"] + _INITIALS_PREFIX_BONUS, 1)
|
|
300
|
+
elif _is_subsequence(name_query, initials):
|
|
301
|
+
r["score"] = round(r["score"] + _INITIALS_BONUS, 1)
|
|
302
|
+
results.sort(key=lambda r: -r["score"])
|
|
303
|
+
return results[:max_results]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""파일 작업 도구."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .base import BaseTool, ToolResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileReadTool(BaseTool):
|
|
12
|
+
"""파일 내용 읽기 도구."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, contents_manager: Any, base_path: str = "."):
|
|
15
|
+
self._contents_manager = contents_manager
|
|
16
|
+
self._base_path = Path(base_path)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return "file_read"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def description(self) -> str:
|
|
24
|
+
return """Read the contents of a file.
|
|
25
|
+
Returns the file content as text. For binary files, returns an error."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def parameters(self) -> dict[str, Any]:
|
|
29
|
+
return {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"path": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "The file path to read (relative to workspace)",
|
|
35
|
+
},
|
|
36
|
+
"encoding": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "File encoding (default: utf-8)",
|
|
39
|
+
"default": "utf-8",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
"required": ["path"],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def risk_level(self) -> str:
|
|
47
|
+
return "low"
|
|
48
|
+
|
|
49
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
50
|
+
"""파일 내용을 읽습니다."""
|
|
51
|
+
path = kwargs.get("path", "")
|
|
52
|
+
encoding = kwargs.get("encoding", "utf-8")
|
|
53
|
+
|
|
54
|
+
if not path:
|
|
55
|
+
return ToolResult(success=False, error="경로가 필요합니다")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
file_path = self._base_path / path
|
|
59
|
+
if not file_path.exists():
|
|
60
|
+
return ToolResult(success=False, error=f"파일을 찾을 수 없음: {path}")
|
|
61
|
+
|
|
62
|
+
if not file_path.is_file():
|
|
63
|
+
return ToolResult(success=False, error=f"파일이 아님: {path}")
|
|
64
|
+
|
|
65
|
+
content = file_path.read_text(encoding=encoding)
|
|
66
|
+
return ToolResult(
|
|
67
|
+
success=True,
|
|
68
|
+
data={"content": content, "path": str(path)},
|
|
69
|
+
metadata={"size": len(content), "encoding": encoding},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
except UnicodeDecodeError:
|
|
73
|
+
return ToolResult(
|
|
74
|
+
success=False, error=f"텍스트로 읽을 수 없는 파일: {path}"
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return ToolResult(success=False, error=f"파일 읽기 오류: {str(e)}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FileWriteTool(BaseTool):
|
|
81
|
+
"""파일 내용 쓰기 도구."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, contents_manager: Any, base_path: str = "."):
|
|
84
|
+
self._contents_manager = contents_manager
|
|
85
|
+
self._base_path = Path(base_path)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def name(self) -> str:
|
|
89
|
+
return "file_write"
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def description(self) -> str:
|
|
93
|
+
return """Write content to a file.
|
|
94
|
+
Creates the file if it doesn't exist. Creates parent directories if needed."""
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def parameters(self) -> dict[str, Any]:
|
|
98
|
+
return {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {
|
|
101
|
+
"path": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "The file path to write (relative to workspace)",
|
|
104
|
+
},
|
|
105
|
+
"content": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "The content to write",
|
|
108
|
+
},
|
|
109
|
+
"encoding": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "File encoding (default: utf-8)",
|
|
112
|
+
"default": "utf-8",
|
|
113
|
+
},
|
|
114
|
+
"append": {
|
|
115
|
+
"type": "boolean",
|
|
116
|
+
"description": "Append to file instead of overwriting",
|
|
117
|
+
"default": False,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
"required": ["path", "content"],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def risk_level(self) -> str:
|
|
125
|
+
return "high"
|
|
126
|
+
|
|
127
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
128
|
+
"""파일 내용을 씁니다."""
|
|
129
|
+
path = kwargs.get("path", "")
|
|
130
|
+
content = kwargs.get("content", "")
|
|
131
|
+
encoding = kwargs.get("encoding", "utf-8")
|
|
132
|
+
append = kwargs.get("append", False)
|
|
133
|
+
|
|
134
|
+
if not path:
|
|
135
|
+
return ToolResult(success=False, error="경로가 필요합니다")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
file_path = self._base_path / path
|
|
139
|
+
|
|
140
|
+
# 필요시 상위 디렉토리 생성
|
|
141
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
|
|
143
|
+
mode = "a" if append else "w"
|
|
144
|
+
with open(file_path, mode, encoding=encoding) as f:
|
|
145
|
+
f.write(content)
|
|
146
|
+
|
|
147
|
+
return ToolResult(
|
|
148
|
+
success=True,
|
|
149
|
+
data={"path": str(path), "operation": "append" if append else "write"},
|
|
150
|
+
metadata={"size": len(content)},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return ToolResult(success=False, error=f"파일 쓰기 오류: {str(e)}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class FileListTool(BaseTool):
|
|
158
|
+
"""디렉토리 내용 목록 도구."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, contents_manager: Any, base_path: str = "."):
|
|
161
|
+
self._contents_manager = contents_manager
|
|
162
|
+
self._base_path = Path(base_path)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def name(self) -> str:
|
|
166
|
+
return "file_list"
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def description(self) -> str:
|
|
170
|
+
return """List files and directories in a path.
|
|
171
|
+
Returns a list of file/directory names with their types."""
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def parameters(self) -> dict[str, Any]:
|
|
175
|
+
return {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"path": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "The directory path to list (relative to workspace)",
|
|
181
|
+
"default": ".",
|
|
182
|
+
},
|
|
183
|
+
"recursive": {
|
|
184
|
+
"type": "boolean",
|
|
185
|
+
"description": "List recursively",
|
|
186
|
+
"default": False,
|
|
187
|
+
},
|
|
188
|
+
"pattern": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": "Glob pattern to filter files (e.g., '*.py')",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def risk_level(self) -> str:
|
|
197
|
+
return "low"
|
|
198
|
+
|
|
199
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
200
|
+
"""디렉토리 내용을 나열합니다."""
|
|
201
|
+
path = kwargs.get("path", ".")
|
|
202
|
+
recursive = kwargs.get("recursive", False)
|
|
203
|
+
pattern = kwargs.get("pattern")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
dir_path = self._base_path / path
|
|
207
|
+
if not dir_path.exists():
|
|
208
|
+
return ToolResult(success=False, error=f"경로를 찾을 수 없음: {path}")
|
|
209
|
+
|
|
210
|
+
if not dir_path.is_dir():
|
|
211
|
+
return ToolResult(success=False, error=f"디렉토리가 아님: {path}")
|
|
212
|
+
|
|
213
|
+
entries = []
|
|
214
|
+
|
|
215
|
+
if pattern:
|
|
216
|
+
if recursive:
|
|
217
|
+
files = dir_path.rglob(pattern)
|
|
218
|
+
else:
|
|
219
|
+
files = dir_path.glob(pattern)
|
|
220
|
+
else:
|
|
221
|
+
if recursive:
|
|
222
|
+
files = dir_path.rglob("*")
|
|
223
|
+
else:
|
|
224
|
+
files = dir_path.iterdir()
|
|
225
|
+
|
|
226
|
+
for entry in files:
|
|
227
|
+
rel_path = entry.relative_to(self._base_path)
|
|
228
|
+
entries.append(
|
|
229
|
+
{
|
|
230
|
+
"name": entry.name,
|
|
231
|
+
"path": str(rel_path),
|
|
232
|
+
"type": "directory" if entry.is_dir() else "file",
|
|
233
|
+
"size": entry.stat().st_size if entry.is_file() else None,
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return ToolResult(
|
|
238
|
+
success=True,
|
|
239
|
+
data={"entries": entries, "path": str(path)},
|
|
240
|
+
metadata={"count": len(entries)},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
return ToolResult(success=False, error=f"디렉토리 목록 조회 오류: {str(e)}")
|