onetool-mcp 1.0.0b1__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.
- bench/__init__.py +5 -0
- bench/cli.py +69 -0
- bench/harness/__init__.py +66 -0
- bench/harness/client.py +692 -0
- bench/harness/config.py +397 -0
- bench/harness/csv_writer.py +109 -0
- bench/harness/evaluate.py +512 -0
- bench/harness/metrics.py +283 -0
- bench/harness/runner.py +899 -0
- bench/py.typed +0 -0
- bench/reporter.py +629 -0
- bench/run.py +487 -0
- bench/secrets.py +101 -0
- bench/utils.py +16 -0
- onetool/__init__.py +4 -0
- onetool/cli.py +391 -0
- onetool/py.typed +0 -0
- onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
- onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
- onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
- onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
- onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
- ot/__init__.py +37 -0
- ot/__main__.py +6 -0
- ot/_cli.py +107 -0
- ot/_tui.py +53 -0
- ot/config/__init__.py +46 -0
- ot/config/defaults/bench.yaml +4 -0
- ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
- ot/config/defaults/diagram-templates/c4-context.puml +30 -0
- ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
- ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
- ot/config/defaults/diagram-templates/microservices.d2 +81 -0
- ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
- ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
- ot/config/defaults/onetool.yaml +25 -0
- ot/config/defaults/prompts.yaml +97 -0
- ot/config/defaults/servers.yaml +7 -0
- ot/config/defaults/snippets.yaml +4 -0
- ot/config/defaults/tool_templates/__init__.py +7 -0
- ot/config/defaults/tool_templates/extension.py +52 -0
- ot/config/defaults/tool_templates/isolated.py +61 -0
- ot/config/dynamic.py +121 -0
- ot/config/global_templates/__init__.py +2 -0
- ot/config/global_templates/bench-secrets-template.yaml +6 -0
- ot/config/global_templates/bench.yaml +9 -0
- ot/config/global_templates/onetool.yaml +27 -0
- ot/config/global_templates/secrets-template.yaml +44 -0
- ot/config/global_templates/servers.yaml +18 -0
- ot/config/global_templates/snippets.yaml +235 -0
- ot/config/loader.py +1087 -0
- ot/config/mcp.py +145 -0
- ot/config/secrets.py +190 -0
- ot/config/tool_config.py +125 -0
- ot/decorators.py +116 -0
- ot/executor/__init__.py +35 -0
- ot/executor/base.py +16 -0
- ot/executor/fence_processor.py +83 -0
- ot/executor/linter.py +142 -0
- ot/executor/pack_proxy.py +260 -0
- ot/executor/param_resolver.py +140 -0
- ot/executor/pep723.py +288 -0
- ot/executor/result_store.py +369 -0
- ot/executor/runner.py +496 -0
- ot/executor/simple.py +163 -0
- ot/executor/tool_loader.py +396 -0
- ot/executor/validator.py +398 -0
- ot/executor/worker_pool.py +388 -0
- ot/executor/worker_proxy.py +189 -0
- ot/http_client.py +145 -0
- ot/logging/__init__.py +37 -0
- ot/logging/config.py +315 -0
- ot/logging/entry.py +213 -0
- ot/logging/format.py +188 -0
- ot/logging/span.py +349 -0
- ot/meta.py +1555 -0
- ot/paths.py +453 -0
- ot/prompts.py +218 -0
- ot/proxy/__init__.py +21 -0
- ot/proxy/manager.py +396 -0
- ot/py.typed +0 -0
- ot/registry/__init__.py +189 -0
- ot/registry/models.py +57 -0
- ot/registry/parser.py +269 -0
- ot/registry/registry.py +413 -0
- ot/server.py +315 -0
- ot/shortcuts/__init__.py +15 -0
- ot/shortcuts/aliases.py +87 -0
- ot/shortcuts/snippets.py +258 -0
- ot/stats/__init__.py +35 -0
- ot/stats/html.py +250 -0
- ot/stats/jsonl_writer.py +283 -0
- ot/stats/reader.py +354 -0
- ot/stats/timing.py +57 -0
- ot/support.py +63 -0
- ot/tools.py +114 -0
- ot/utils/__init__.py +81 -0
- ot/utils/batch.py +161 -0
- ot/utils/cache.py +120 -0
- ot/utils/deps.py +403 -0
- ot/utils/exceptions.py +23 -0
- ot/utils/factory.py +179 -0
- ot/utils/format.py +65 -0
- ot/utils/http.py +202 -0
- ot/utils/platform.py +45 -0
- ot/utils/sanitize.py +130 -0
- ot/utils/truncate.py +69 -0
- ot_tools/__init__.py +4 -0
- ot_tools/_convert/__init__.py +12 -0
- ot_tools/_convert/excel.py +279 -0
- ot_tools/_convert/pdf.py +254 -0
- ot_tools/_convert/powerpoint.py +268 -0
- ot_tools/_convert/utils.py +358 -0
- ot_tools/_convert/word.py +283 -0
- ot_tools/brave_search.py +604 -0
- ot_tools/code_search.py +736 -0
- ot_tools/context7.py +495 -0
- ot_tools/convert.py +614 -0
- ot_tools/db.py +415 -0
- ot_tools/diagram.py +1604 -0
- ot_tools/diagram.yaml +167 -0
- ot_tools/excel.py +1372 -0
- ot_tools/file.py +1348 -0
- ot_tools/firecrawl.py +732 -0
- ot_tools/grounding_search.py +646 -0
- ot_tools/package.py +604 -0
- ot_tools/py.typed +0 -0
- ot_tools/ripgrep.py +544 -0
- ot_tools/scaffold.py +471 -0
- ot_tools/transform.py +213 -0
- ot_tools/web_fetch.py +384 -0
ot/paths.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Path resolution for OneTool global and project directories.
|
|
2
|
+
|
|
3
|
+
OneTool uses a three-tier directory structure:
|
|
4
|
+
- Bundled: package data in ot.config.defaults — read-only defaults
|
|
5
|
+
- Global: ~/.onetool/ — user-wide settings, secrets
|
|
6
|
+
- Project: .onetool/ — project-specific config
|
|
7
|
+
|
|
8
|
+
Each .onetool/ directory uses subdirectories to organise files by purpose:
|
|
9
|
+
- config/ — YAML configuration files
|
|
10
|
+
- logs/ — Application log files
|
|
11
|
+
- stats/ — Statistics data (stats.jsonl)
|
|
12
|
+
- sessions/ — Browser session state
|
|
13
|
+
- tools/ — Reserved for installed tool packs
|
|
14
|
+
|
|
15
|
+
Directories are created lazily on first use, not on install.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from importlib import resources
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# Directory names
|
|
26
|
+
GLOBAL_DIR_NAME = ".onetool"
|
|
27
|
+
PROJECT_DIR_NAME = ".onetool"
|
|
28
|
+
|
|
29
|
+
# Subdirectory names within .onetool/
|
|
30
|
+
CONFIG_SUBDIR = "config"
|
|
31
|
+
LOGS_SUBDIR = "logs"
|
|
32
|
+
STATS_SUBDIR = "stats"
|
|
33
|
+
SESSIONS_SUBDIR = "sessions"
|
|
34
|
+
TOOLS_SUBDIR = "tools"
|
|
35
|
+
|
|
36
|
+
# Package containing bundled config defaults
|
|
37
|
+
BUNDLED_CONFIG_PACKAGE = "ot.config.defaults"
|
|
38
|
+
|
|
39
|
+
# Package containing global templates (copied to ~/.onetool/ on first run)
|
|
40
|
+
GLOBAL_TEMPLATES_PACKAGE = "ot.config.global_templates"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resolve_package_dir(package_name: str, description: str) -> Path:
|
|
44
|
+
"""Resolve a package to a filesystem directory path.
|
|
45
|
+
|
|
46
|
+
Uses importlib.resources to access package data. Works correctly across:
|
|
47
|
+
- Regular pip/uv install (wheel)
|
|
48
|
+
- Editable install (uv tool install -e .)
|
|
49
|
+
- Development mode
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
package_name: Dotted package name (e.g., "ot.config.defaults")
|
|
53
|
+
description: Human-readable description for error messages
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Path to package directory (read-only package data)
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
FileNotFoundError: If package is not found or not on filesystem
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
files = resources.files(package_name)
|
|
63
|
+
except (ModuleNotFoundError, TypeError) as e:
|
|
64
|
+
raise FileNotFoundError(
|
|
65
|
+
f"{description} package not found: {package_name}. "
|
|
66
|
+
"Ensure onetool is properly installed."
|
|
67
|
+
) from e
|
|
68
|
+
|
|
69
|
+
# Try multiple approaches to get a filesystem path from the Traversable.
|
|
70
|
+
# importlib.resources returns different types depending on install mode:
|
|
71
|
+
# - Regular install: pathlib.Path-like object
|
|
72
|
+
# - Editable install: MultiplexedPath (internal type)
|
|
73
|
+
# - Zipped package: ZipPath (would need extraction)
|
|
74
|
+
|
|
75
|
+
# Approach 1: Direct _path attribute (MultiplexedPath in editable installs)
|
|
76
|
+
if hasattr(files, "_path"):
|
|
77
|
+
path = Path(files._path)
|
|
78
|
+
if path.is_dir():
|
|
79
|
+
return path
|
|
80
|
+
|
|
81
|
+
# Approach 2: String conversion (works for regular Path-like objects)
|
|
82
|
+
path_str = str(files)
|
|
83
|
+
|
|
84
|
+
# Skip if it looks like a repr() output rather than a path
|
|
85
|
+
if not path_str.startswith(("MultiplexedPath(", "<", "{")):
|
|
86
|
+
path = Path(path_str)
|
|
87
|
+
if path.is_dir():
|
|
88
|
+
return path
|
|
89
|
+
|
|
90
|
+
# Approach 3: Extract path from MultiplexedPath repr as last resort
|
|
91
|
+
if path_str.startswith("MultiplexedPath("):
|
|
92
|
+
import re
|
|
93
|
+
|
|
94
|
+
match = re.search(r"'([^']+)'", path_str)
|
|
95
|
+
if match:
|
|
96
|
+
path = Path(match.group(1))
|
|
97
|
+
if path.is_dir():
|
|
98
|
+
return path
|
|
99
|
+
|
|
100
|
+
# If we get here, the package exists but isn't on a real filesystem
|
|
101
|
+
# (e.g., inside a zipfile). This is not supported.
|
|
102
|
+
raise FileNotFoundError(
|
|
103
|
+
f"{description} directory exists but is not on filesystem: {path_str}. "
|
|
104
|
+
"OneTool requires installation from an unpacked wheel, not a zipfile."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_bundled_config_dir() -> Path:
|
|
109
|
+
"""Get the bundled config defaults directory path.
|
|
110
|
+
|
|
111
|
+
Uses importlib.resources to access package data. Works correctly across:
|
|
112
|
+
- Regular pip/uv install (wheel)
|
|
113
|
+
- Editable install (uv tool install -e .)
|
|
114
|
+
- Development mode
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Path to bundled defaults directory (read-only package data)
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
FileNotFoundError: If bundled defaults package is not found or not on filesystem
|
|
121
|
+
"""
|
|
122
|
+
return _resolve_package_dir(BUNDLED_CONFIG_PACKAGE, "Bundled config")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_global_templates_dir() -> Path:
|
|
126
|
+
"""Get the global templates directory path.
|
|
127
|
+
|
|
128
|
+
Global templates are user-facing config files with commented examples,
|
|
129
|
+
copied to ~/.onetool/ on first run. Unlike bundled defaults (which are
|
|
130
|
+
minimal working configs), these provide rich documentation and examples.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Path to global templates directory (read-only package data)
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
FileNotFoundError: If global templates package is not found or not on filesystem
|
|
137
|
+
"""
|
|
138
|
+
return _resolve_package_dir(GLOBAL_TEMPLATES_PACKAGE, "Global templates")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_effective_cwd() -> Path:
|
|
142
|
+
"""Get the effective working directory.
|
|
143
|
+
|
|
144
|
+
Returns OT_CWD if set, else Path.cwd(). This provides a single point
|
|
145
|
+
of control for working directory resolution across all CLIs.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Resolved Path for working directory
|
|
149
|
+
"""
|
|
150
|
+
env_cwd = os.getenv("OT_CWD")
|
|
151
|
+
if env_cwd:
|
|
152
|
+
return Path(env_cwd).resolve()
|
|
153
|
+
return Path.cwd()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_global_dir() -> Path:
|
|
157
|
+
"""Get the global OneTool directory path.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Path to ~/.onetool/ (not necessarily existing)
|
|
161
|
+
"""
|
|
162
|
+
return Path.home() / GLOBAL_DIR_NAME
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_project_dir(start: Path | None = None) -> Path | None:
|
|
166
|
+
"""Get the project OneTool directory.
|
|
167
|
+
|
|
168
|
+
Returns cwd/.onetool if it exists, else None. No tree-walking.
|
|
169
|
+
Uses get_effective_cwd() if start is not provided.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
start: Starting directory (default: get_effective_cwd())
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Path to .onetool/ if found, None otherwise
|
|
176
|
+
"""
|
|
177
|
+
cwd = start or get_effective_cwd()
|
|
178
|
+
candidate = cwd / PROJECT_DIR_NAME
|
|
179
|
+
if candidate.is_dir():
|
|
180
|
+
return candidate
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_template_files() -> list[tuple[Path, str]]:
|
|
185
|
+
"""Get list of template files that would be copied to global dir.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of (source_path, dest_name) tuples for each template file.
|
|
189
|
+
dest_name has -template suffix stripped (e.g., secrets-template.yaml -> secrets.yaml)
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
templates_dir = get_global_templates_dir()
|
|
193
|
+
result = []
|
|
194
|
+
for config_file in templates_dir.glob("*.yaml"):
|
|
195
|
+
dest_name = config_file.name.replace("-template.yaml", ".yaml")
|
|
196
|
+
result.append((config_file, dest_name))
|
|
197
|
+
return result
|
|
198
|
+
except FileNotFoundError:
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def create_backup(file_path: Path) -> Path:
|
|
203
|
+
"""Create a numbered backup of a file.
|
|
204
|
+
|
|
205
|
+
Creates backups as file.bak, file.bak.1, file.bak.2, etc.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
file_path: Path to the file to backup
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Path to the created backup file
|
|
212
|
+
"""
|
|
213
|
+
backup_base = file_path.with_suffix(file_path.suffix + ".bak")
|
|
214
|
+
|
|
215
|
+
# Find the next available backup number
|
|
216
|
+
if not backup_base.exists():
|
|
217
|
+
backup_path = backup_base
|
|
218
|
+
else:
|
|
219
|
+
n = 1
|
|
220
|
+
while True:
|
|
221
|
+
backup_path = backup_base.with_suffix(f".bak.{n}")
|
|
222
|
+
if not backup_path.exists():
|
|
223
|
+
break
|
|
224
|
+
n += 1
|
|
225
|
+
|
|
226
|
+
import shutil
|
|
227
|
+
|
|
228
|
+
shutil.copy2(file_path, backup_path)
|
|
229
|
+
return backup_path
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def ensure_global_dir(quiet: bool = False, force: bool = False) -> Path:
|
|
233
|
+
"""Ensure the global OneTool directory exists with subdirectory structure.
|
|
234
|
+
|
|
235
|
+
Creates ~/.onetool/ with subdirectories (config/, logs/, stats/, sessions/, tools/)
|
|
236
|
+
and copies template config files from global_templates to config/.
|
|
237
|
+
Templates are user-facing files with commented examples for customization.
|
|
238
|
+
Subdirectories (like diagram-templates/) are NOT copied - they remain in
|
|
239
|
+
bundled defaults and are accessed via config inheritance.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
quiet: Suppress creation messages
|
|
243
|
+
force: Overwrite existing files (for reset functionality)
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Path to ~/.onetool/
|
|
247
|
+
"""
|
|
248
|
+
import shutil
|
|
249
|
+
|
|
250
|
+
global_dir = get_global_dir()
|
|
251
|
+
|
|
252
|
+
# If directory exists and not forcing, return early
|
|
253
|
+
if global_dir.exists() and not force:
|
|
254
|
+
return global_dir
|
|
255
|
+
|
|
256
|
+
# Create directory structure with subdirectories
|
|
257
|
+
global_dir.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
subdirs = [CONFIG_SUBDIR, LOGS_SUBDIR, STATS_SUBDIR, SESSIONS_SUBDIR, TOOLS_SUBDIR]
|
|
259
|
+
for subdir in subdirs:
|
|
260
|
+
(global_dir / subdir).mkdir(exist_ok=True)
|
|
261
|
+
|
|
262
|
+
# Copy template config files to config/ subdirectory
|
|
263
|
+
# Only YAML files are copied; subdirectories stay in bundled defaults
|
|
264
|
+
# Files named *-template.yaml are copied without the -template suffix
|
|
265
|
+
# (to avoid gitignore patterns on secrets.yaml)
|
|
266
|
+
config_dir = global_dir / CONFIG_SUBDIR
|
|
267
|
+
copied_items: list[str] = []
|
|
268
|
+
try:
|
|
269
|
+
templates_dir = get_global_templates_dir()
|
|
270
|
+
for config_file in templates_dir.glob("*.yaml"):
|
|
271
|
+
# Strip -template suffix if present (e.g., secrets-template.yaml -> secrets.yaml)
|
|
272
|
+
dest_name = config_file.name.replace("-template.yaml", ".yaml")
|
|
273
|
+
dest = config_dir / dest_name
|
|
274
|
+
# Copy if doesn't exist, or if forcing
|
|
275
|
+
if not dest.exists() or force:
|
|
276
|
+
shutil.copy(config_file, dest)
|
|
277
|
+
copied_items.append(f"config/{dest_name}")
|
|
278
|
+
except FileNotFoundError:
|
|
279
|
+
# Global templates not available (dev environment without package install)
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
if not quiet:
|
|
283
|
+
# Use stderr to avoid interfering with MCP stdout
|
|
284
|
+
action = "Resetting" if force else "Creating"
|
|
285
|
+
print(f"{action} {global_dir}/", file=sys.stderr)
|
|
286
|
+
for subdir in subdirs:
|
|
287
|
+
print(f" ✓ {subdir}/", file=sys.stderr)
|
|
288
|
+
for item_name in copied_items:
|
|
289
|
+
print(f" ✓ {item_name}", file=sys.stderr)
|
|
290
|
+
|
|
291
|
+
return global_dir
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def ensure_project_dir(path: Path | None = None, quiet: bool = False) -> Path:
|
|
295
|
+
"""Ensure the project OneTool directory exists with subdirectory structure.
|
|
296
|
+
|
|
297
|
+
Creates .onetool/ in the specified directory or effective cwd,
|
|
298
|
+
including subdirectories (config/, logs/, stats/, sessions/, tools/).
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
path: Project root (default: get_effective_cwd())
|
|
302
|
+
quiet: Suppress creation messages
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Path to .onetool/
|
|
306
|
+
"""
|
|
307
|
+
project_root = path or get_effective_cwd()
|
|
308
|
+
project_dir = project_root / PROJECT_DIR_NAME
|
|
309
|
+
|
|
310
|
+
if project_dir.exists():
|
|
311
|
+
return project_dir
|
|
312
|
+
|
|
313
|
+
# Create directory structure with subdirectories
|
|
314
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
315
|
+
subdirs = [CONFIG_SUBDIR, LOGS_SUBDIR, STATS_SUBDIR, SESSIONS_SUBDIR, TOOLS_SUBDIR]
|
|
316
|
+
for subdir in subdirs:
|
|
317
|
+
(project_dir / subdir).mkdir(exist_ok=True)
|
|
318
|
+
|
|
319
|
+
if not quiet:
|
|
320
|
+
print(f"Creating {project_dir.relative_to(project_root)}/", file=sys.stderr)
|
|
321
|
+
for subdir in subdirs:
|
|
322
|
+
print(f" ✓ {subdir}/", file=sys.stderr)
|
|
323
|
+
|
|
324
|
+
return project_dir
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_config_path(cli_name: str, scope: str = "any") -> Path | None:
|
|
328
|
+
"""Get the config file path for a CLI.
|
|
329
|
+
|
|
330
|
+
Resolution order for scope="any":
|
|
331
|
+
1. cwd/.onetool/config/<cli>.yaml (project-specific)
|
|
332
|
+
2. ~/.onetool/config/<cli>.yaml (global)
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
cli_name: CLI name (e.g., "onetool", "bench")
|
|
336
|
+
scope: "global", "project", or "any" (project first, then global)
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Path to config file if found, None otherwise
|
|
340
|
+
"""
|
|
341
|
+
config_name = f"{cli_name}.yaml"
|
|
342
|
+
|
|
343
|
+
if scope == "project" or scope == "any":
|
|
344
|
+
cwd = get_effective_cwd()
|
|
345
|
+
project_config = cwd / PROJECT_DIR_NAME / CONFIG_SUBDIR / config_name
|
|
346
|
+
if project_config.exists():
|
|
347
|
+
return project_config
|
|
348
|
+
|
|
349
|
+
if scope == "global" or scope == "any":
|
|
350
|
+
global_dir = get_global_dir()
|
|
351
|
+
global_config = global_dir / CONFIG_SUBDIR / config_name
|
|
352
|
+
if global_config.exists():
|
|
353
|
+
return global_config
|
|
354
|
+
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def expand_path(path: str) -> Path:
|
|
359
|
+
"""Expand ~ in a path.
|
|
360
|
+
|
|
361
|
+
Only expands ~ to home directory. Does NOT expand ${VAR} patterns.
|
|
362
|
+
Use ~/path instead of ${HOME}/path.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
path: Path string potentially containing ~
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Expanded absolute Path
|
|
369
|
+
"""
|
|
370
|
+
return Path(path).expanduser().resolve()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_config_dir(base_dir: Path | None = None) -> Path:
|
|
374
|
+
"""Get the config directory path within a .onetool directory.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
base_dir: Base .onetool directory (default: global dir)
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Path to config/ subdirectory
|
|
381
|
+
"""
|
|
382
|
+
base = base_dir or get_global_dir()
|
|
383
|
+
return base / CONFIG_SUBDIR
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def get_logs_dir(base_dir: Path | None = None) -> Path:
|
|
387
|
+
"""Get the logs directory path within a .onetool directory.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
base_dir: Base .onetool directory (default: global dir)
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Path to logs/ subdirectory
|
|
394
|
+
"""
|
|
395
|
+
base = base_dir or get_global_dir()
|
|
396
|
+
return base / LOGS_SUBDIR
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def get_stats_dir(base_dir: Path | None = None) -> Path:
|
|
400
|
+
"""Get the stats directory path within a .onetool directory.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
base_dir: Base .onetool directory (default: global dir)
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Path to stats/ subdirectory
|
|
407
|
+
"""
|
|
408
|
+
base = base_dir or get_global_dir()
|
|
409
|
+
return base / STATS_SUBDIR
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_sessions_dir(base_dir: Path | None = None) -> Path:
|
|
413
|
+
"""Get the sessions directory path within a .onetool directory.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
base_dir: Base .onetool directory (default: global dir)
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Path to sessions/ subdirectory
|
|
420
|
+
"""
|
|
421
|
+
base = base_dir or get_global_dir()
|
|
422
|
+
return base / SESSIONS_SUBDIR
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def resolve_cwd_path(path: str) -> Path:
|
|
426
|
+
"""Resolve a path relative to the project working directory (OT_CWD).
|
|
427
|
+
|
|
428
|
+
Use this for reading/writing files in the user's project.
|
|
429
|
+
This is the internal version for in-process tools - uses OT_CWD env var directly.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
path: Path string (relative, absolute, or with ~)
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Resolved absolute Path
|
|
436
|
+
|
|
437
|
+
Behaviour:
|
|
438
|
+
- ~ paths: expanded to home directory
|
|
439
|
+
- Absolute paths: returned unchanged
|
|
440
|
+
- Relative paths: resolved relative to get_effective_cwd()
|
|
441
|
+
|
|
442
|
+
Example:
|
|
443
|
+
>>> resolve_cwd_path("data/file.txt")
|
|
444
|
+
PosixPath('/project/data/file.txt')
|
|
445
|
+
>>> resolve_cwd_path("/tmp/output.txt")
|
|
446
|
+
PosixPath('/tmp/output.txt')
|
|
447
|
+
>>> resolve_cwd_path("~/output.txt")
|
|
448
|
+
PosixPath('/home/user/output.txt')
|
|
449
|
+
"""
|
|
450
|
+
p = Path(path).expanduser()
|
|
451
|
+
if p.is_absolute():
|
|
452
|
+
return p.resolve()
|
|
453
|
+
return (get_effective_cwd() / p).resolve()
|
ot/prompts.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Prompts loader for externalized MCP server instructions.
|
|
2
|
+
|
|
3
|
+
Loads prompts from prompts.yaml. File must exist and contain instructions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from loguru import logger
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolPrompt(BaseModel):
|
|
17
|
+
"""Prompt configuration for a specific tool."""
|
|
18
|
+
|
|
19
|
+
description: str | None = Field(
|
|
20
|
+
default=None, description="Override tool description"
|
|
21
|
+
)
|
|
22
|
+
examples: list[str] = Field(default_factory=list, description="Usage examples")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PromptsConfig(BaseModel):
|
|
26
|
+
"""Configuration for MCP server prompts and tool descriptions."""
|
|
27
|
+
|
|
28
|
+
instructions: str = Field(
|
|
29
|
+
description="Main server instructions shown to the LLM",
|
|
30
|
+
)
|
|
31
|
+
tools: dict[str, ToolPrompt] = Field(
|
|
32
|
+
default_factory=dict,
|
|
33
|
+
description="Per-tool prompt overrides",
|
|
34
|
+
)
|
|
35
|
+
templates: dict[str, str] = Field(
|
|
36
|
+
default_factory=dict,
|
|
37
|
+
description="Reusable prompt templates with {variable} placeholders",
|
|
38
|
+
)
|
|
39
|
+
packs: dict[str, str] = Field(
|
|
40
|
+
default_factory=dict,
|
|
41
|
+
description="Per-pack instructions (e.g., excel, github)",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PromptsError(Exception):
|
|
46
|
+
"""Error loading prompts configuration."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_bundled_prompts_path() -> Path:
|
|
50
|
+
"""Get path to bundled default prompts.yaml."""
|
|
51
|
+
return Path(__file__).parent / "config" / "defaults" / "prompts.yaml"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_prompts(prompts_path: Path | str | None = None) -> PromptsConfig:
|
|
55
|
+
"""Load prompts configuration from YAML file.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
prompts_path: Path to prompts file. Falls back to bundled default.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
PromptsConfig with loaded prompts.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
PromptsError: If file is invalid or has no instructions.
|
|
65
|
+
"""
|
|
66
|
+
if prompts_path is not None:
|
|
67
|
+
prompts_path = Path(prompts_path)
|
|
68
|
+
if not prompts_path.exists():
|
|
69
|
+
raise PromptsError(f"Prompts file not found: {prompts_path}")
|
|
70
|
+
else:
|
|
71
|
+
# Try config/prompts.yaml, fall back to bundled default
|
|
72
|
+
prompts_path = Path("config/prompts.yaml")
|
|
73
|
+
if not prompts_path.exists():
|
|
74
|
+
prompts_path = _get_bundled_prompts_path()
|
|
75
|
+
|
|
76
|
+
logger.debug(f"Loading prompts from {prompts_path}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with prompts_path.open() as f:
|
|
80
|
+
raw_data = yaml.safe_load(f)
|
|
81
|
+
except yaml.YAMLError as e:
|
|
82
|
+
raise PromptsError(f"Invalid YAML in {prompts_path}: {e}") from e
|
|
83
|
+
except OSError as e:
|
|
84
|
+
raise PromptsError(f"Error reading {prompts_path}: {e}") from e
|
|
85
|
+
|
|
86
|
+
if raw_data is None or not isinstance(raw_data, dict):
|
|
87
|
+
raise PromptsError(f"Empty or invalid prompts file: {prompts_path}")
|
|
88
|
+
|
|
89
|
+
# Handle nested 'prompts:' key (used in bundled default)
|
|
90
|
+
if "prompts" in raw_data and isinstance(raw_data["prompts"], dict):
|
|
91
|
+
raw_data = raw_data["prompts"]
|
|
92
|
+
|
|
93
|
+
if "instructions" not in raw_data or not raw_data["instructions"]:
|
|
94
|
+
raise PromptsError(f"Missing 'instructions' in {prompts_path}")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return PromptsConfig.model_validate(raw_data)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise PromptsError(f"Invalid prompts configuration: {e}") from e
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render_template(
|
|
103
|
+
config: PromptsConfig, template_name: str, **kwargs: Any
|
|
104
|
+
) -> str | None:
|
|
105
|
+
"""Render a prompt template with variable substitution.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
config: PromptsConfig with templates
|
|
109
|
+
template_name: Name of the template to render
|
|
110
|
+
**kwargs: Variables to substitute in the template
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Rendered template string, or None if template not found.
|
|
114
|
+
"""
|
|
115
|
+
template = config.templates.get(template_name)
|
|
116
|
+
if template is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
return template.format(**kwargs)
|
|
121
|
+
except KeyError as e:
|
|
122
|
+
logger.warning(f"Missing template variable: {e}")
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_tool_description(
|
|
127
|
+
config: PromptsConfig, tool_name: str, default: str = ""
|
|
128
|
+
) -> str:
|
|
129
|
+
"""Get tool description from prompts config with fallback to docstring.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
config: PromptsConfig with tool prompts
|
|
133
|
+
tool_name: Name of the tool
|
|
134
|
+
default: Default description if not in config (typically from docstring)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Tool description string.
|
|
138
|
+
"""
|
|
139
|
+
tool_prompt = config.tools.get(tool_name)
|
|
140
|
+
if tool_prompt and tool_prompt.description:
|
|
141
|
+
return tool_prompt.description
|
|
142
|
+
return default
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_tool_examples(config: PromptsConfig, tool_name: str) -> list[str]:
|
|
146
|
+
"""Get usage examples for a tool.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
config: PromptsConfig with tool prompts
|
|
150
|
+
tool_name: Name of the tool
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of example strings.
|
|
154
|
+
"""
|
|
155
|
+
tool_prompt = config.tools.get(tool_name)
|
|
156
|
+
if tool_prompt:
|
|
157
|
+
return tool_prompt.examples
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_pack_instructions(config: PromptsConfig, pack: str) -> str | None:
|
|
162
|
+
"""Get instructions for a pack from prompts config.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
config: PromptsConfig with pack instructions
|
|
166
|
+
pack: Name of the pack (e.g., "excel", "github")
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Pack instructions string, or None if not configured.
|
|
170
|
+
"""
|
|
171
|
+
return config.packs.get(pack)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Global prompts instance
|
|
175
|
+
_prompts: PromptsConfig | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_prompts(
|
|
179
|
+
prompts_path: Path | str | None = None,
|
|
180
|
+
inline_prompts: dict[str, Any] | None = None,
|
|
181
|
+
reload: bool = False,
|
|
182
|
+
) -> PromptsConfig:
|
|
183
|
+
"""Get or load the global prompts configuration.
|
|
184
|
+
|
|
185
|
+
Prompts are loaded with the following priority:
|
|
186
|
+
1. Inline prompts (if provided)
|
|
187
|
+
2. prompts_file (from config or explicit path)
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
prompts_path: Path to prompts file (only used on first load)
|
|
191
|
+
inline_prompts: Inline prompts dict from config (overrides file)
|
|
192
|
+
reload: Force reload configuration
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
PromptsConfig instance
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
PromptsError: If prompts cannot be loaded.
|
|
199
|
+
"""
|
|
200
|
+
global _prompts
|
|
201
|
+
|
|
202
|
+
if _prompts is None or reload:
|
|
203
|
+
if inline_prompts is not None:
|
|
204
|
+
# Use inline prompts from config
|
|
205
|
+
if (
|
|
206
|
+
"instructions" not in inline_prompts
|
|
207
|
+
or not inline_prompts["instructions"]
|
|
208
|
+
):
|
|
209
|
+
raise PromptsError("Missing 'instructions' in inline prompts")
|
|
210
|
+
try:
|
|
211
|
+
_prompts = PromptsConfig.model_validate(inline_prompts)
|
|
212
|
+
logger.debug("Using inline prompts from config")
|
|
213
|
+
except Exception as e:
|
|
214
|
+
raise PromptsError(f"Invalid inline prompts: {e}") from e
|
|
215
|
+
else:
|
|
216
|
+
_prompts = load_prompts(prompts_path)
|
|
217
|
+
|
|
218
|
+
return _prompts
|
ot/proxy/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""MCP Proxy module for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides connectivity to external MCP servers that are proxied
|
|
4
|
+
through OneTool's single `run` tool interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ot.proxy.manager import (
|
|
8
|
+
ProxyManager,
|
|
9
|
+
ProxyToolInfo,
|
|
10
|
+
get_proxy_manager,
|
|
11
|
+
reconnect_proxy_manager,
|
|
12
|
+
reset_proxy_manager,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ProxyManager",
|
|
17
|
+
"ProxyToolInfo",
|
|
18
|
+
"get_proxy_manager",
|
|
19
|
+
"reconnect_proxy_manager",
|
|
20
|
+
"reset_proxy_manager",
|
|
21
|
+
]
|