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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. ot_tools/web_fetch.py +384 -0
ot/stats/reader.py ADDED
@@ -0,0 +1,354 @@
1
+ """Stats reader with aggregation and filtering.
2
+
3
+ Reads JSONL stats and aggregates by period with savings calculations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from dataclasses import dataclass
10
+ from datetime import UTC, datetime, timedelta
11
+ from typing import TYPE_CHECKING, Any, Literal
12
+
13
+ from loguru import logger
14
+
15
+ if TYPE_CHECKING:
16
+ from pathlib import Path
17
+
18
+ Period = Literal["day", "week", "month", "all"]
19
+
20
+
21
+ @dataclass
22
+ class ToolStats:
23
+ """Aggregated statistics for a single tool."""
24
+
25
+ tool: str
26
+ total_calls: int
27
+ success_count: int
28
+ error_count: int
29
+ total_chars_in: int
30
+ total_chars_out: int
31
+ total_duration_ms: int
32
+ avg_duration_ms: float
33
+
34
+ @property
35
+ def success_rate(self) -> float:
36
+ """Calculate success rate as percentage."""
37
+ if self.total_calls == 0:
38
+ return 0.0
39
+ return (self.success_count / self.total_calls) * 100
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ """Convert to dictionary representation."""
43
+ return {
44
+ "tool": self.tool,
45
+ "total_calls": self.total_calls,
46
+ "success_count": self.success_count,
47
+ "error_count": self.error_count,
48
+ "success_rate": round(self.success_rate, 1),
49
+ "total_chars_in": self.total_chars_in,
50
+ "total_chars_out": self.total_chars_out,
51
+ "total_duration_ms": self.total_duration_ms,
52
+ "avg_duration_ms": round(self.avg_duration_ms, 1),
53
+ }
54
+
55
+
56
+ # Cost per coffee for savings display (hardcoded)
57
+ COFFEE_COST_USD = 5.0
58
+
59
+
60
+ @dataclass
61
+ class AggregatedStats:
62
+ """Aggregated statistics summary."""
63
+
64
+ period: Period
65
+ start_time: str | None
66
+ end_time: str | None
67
+ total_calls: int
68
+ success_count: int
69
+ error_count: int
70
+ total_chars_in: int
71
+ total_chars_out: int
72
+ total_duration_ms: int
73
+ context_saved: int
74
+ time_saved_ms: int
75
+ tools: list[ToolStats]
76
+ model: str = ""
77
+ cost_estimate_usd: float = 0.0
78
+ savings_usd: float = 0.0
79
+
80
+ @property
81
+ def success_rate(self) -> float:
82
+ """Calculate overall success rate as percentage."""
83
+ if self.total_calls == 0:
84
+ return 0.0
85
+ return (self.success_count / self.total_calls) * 100
86
+
87
+ @property
88
+ def coffees(self) -> float:
89
+ """Calculate coffee equivalent of savings."""
90
+ return self.savings_usd / COFFEE_COST_USD
91
+
92
+ def to_dict(self) -> dict[str, Any]:
93
+ """Convert to dictionary representation."""
94
+ return {
95
+ "period": self.period,
96
+ "start_time": self.start_time,
97
+ "end_time": self.end_time,
98
+ "total_calls": self.total_calls,
99
+ "success_count": self.success_count,
100
+ "error_count": self.error_count,
101
+ "success_rate": round(self.success_rate, 1),
102
+ "total_chars_in": self.total_chars_in,
103
+ "total_chars_out": self.total_chars_out,
104
+ "total_duration_ms": self.total_duration_ms,
105
+ "context_saved": self.context_saved,
106
+ "time_saved_ms": self.time_saved_ms,
107
+ "model": self.model,
108
+ "cost_estimate_usd": round(self.cost_estimate_usd, 4),
109
+ "savings_usd": round(self.savings_usd, 2),
110
+ "coffees": round(self.coffees, 1),
111
+ "tools": [t.to_dict() for t in self.tools],
112
+ }
113
+
114
+
115
+ class StatsReader:
116
+ """Reads and aggregates statistics from JSONL.
117
+
118
+ Usage:
119
+ reader = StatsReader(path, context_per_call=30000, time_overhead_ms=4000)
120
+ stats = reader.read(period="week", tool="brave.search")
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ path: Path,
126
+ context_per_call: int = 30000,
127
+ time_overhead_per_call_ms: int = 4000,
128
+ model: str = "anthropic/claude-opus-4.5",
129
+ cost_per_million_input_tokens: float = 15.0,
130
+ cost_per_million_output_tokens: float = 75.0,
131
+ chars_per_token: float = 4.0,
132
+ ) -> None:
133
+ """Initialize reader.
134
+
135
+ Args:
136
+ path: Path to JSONL file
137
+ context_per_call: Context tokens saved per consolidated call
138
+ time_overhead_per_call_ms: Time overhead in ms saved per call
139
+ model: Model name for cost estimation
140
+ cost_per_million_input_tokens: Cost in USD per million input tokens
141
+ cost_per_million_output_tokens: Cost in USD per million output tokens
142
+ chars_per_token: Average characters per token for estimation
143
+ """
144
+ self._path = path
145
+ self._context_per_call = context_per_call
146
+ self._time_overhead_ms = time_overhead_per_call_ms
147
+ self._model = model
148
+ self._cost_per_m_input = cost_per_million_input_tokens
149
+ self._cost_per_m_output = cost_per_million_output_tokens
150
+ self._chars_per_token = chars_per_token
151
+
152
+ def read(
153
+ self,
154
+ period: Period = "all",
155
+ tool: str | None = None,
156
+ ) -> AggregatedStats:
157
+ """Read and aggregate stats.
158
+
159
+ Args:
160
+ period: Time period to filter (day/week/month/all)
161
+ tool: Optional tool name filter
162
+
163
+ Returns:
164
+ Aggregated statistics
165
+ """
166
+ records = self._load_records()
167
+ filtered = self._filter_records(records, period, tool)
168
+ return self._aggregate(filtered, period)
169
+
170
+ def _load_records(self) -> list[dict[str, Any]]:
171
+ """Load all records from JSONL."""
172
+ if not self._path.exists():
173
+ logger.debug(f"Stats file not found: {self._path}")
174
+ return []
175
+
176
+ records: list[dict[str, Any]] = []
177
+ try:
178
+ with self._path.open() as f:
179
+ for line in f:
180
+ line = line.strip()
181
+ if line:
182
+ try:
183
+ records.append(json.loads(line))
184
+ except json.JSONDecodeError:
185
+ logger.debug(f"Skipping malformed JSON line: {line[:50]}")
186
+ except Exception as e:
187
+ logger.warning(f"Failed to read stats: {e}")
188
+ return []
189
+
190
+ return records
191
+
192
+ def _filter_records(
193
+ self,
194
+ records: list[dict[str, Any]],
195
+ period: Period,
196
+ tool: str | None,
197
+ ) -> list[dict[str, Any]]:
198
+ """Filter records by period and tool."""
199
+ if not records:
200
+ return []
201
+
202
+ # Calculate period cutoff
203
+ cutoff = self._get_period_cutoff(period)
204
+
205
+ filtered: list[dict[str, Any]] = []
206
+ for record in records:
207
+ # Filter by period
208
+ if cutoff is not None:
209
+ try:
210
+ ts = datetime.fromisoformat(record["ts"])
211
+ if ts < cutoff:
212
+ continue
213
+ except (KeyError, ValueError):
214
+ continue
215
+
216
+ # Filter by tool (only applies to tool-type records)
217
+ if (
218
+ tool is not None
219
+ and record.get("type") == "tool"
220
+ and record.get("tool") != tool
221
+ ):
222
+ continue
223
+
224
+ filtered.append(record)
225
+
226
+ return filtered
227
+
228
+ def _get_period_cutoff(self, period: Period) -> datetime | None:
229
+ """Get cutoff datetime for period."""
230
+ if period == "all":
231
+ return None
232
+
233
+ now = datetime.now(UTC)
234
+ if period == "day":
235
+ return now - timedelta(days=1)
236
+ elif period == "week":
237
+ return now - timedelta(weeks=1)
238
+ elif period == "month":
239
+ return now - timedelta(days=30)
240
+
241
+ return None
242
+
243
+ def _aggregate(
244
+ self, records: list[dict[str, Any]], period: Period
245
+ ) -> AggregatedStats:
246
+ """Aggregate records into summary stats.
247
+
248
+ Records are split by type:
249
+ - "run" records: contain chars_in/chars_out, used for run counts and savings
250
+ - "tool" records: contain tool name, used for per-tool breakdown
251
+ """
252
+ if not records:
253
+ return AggregatedStats(
254
+ period=period,
255
+ start_time=None,
256
+ end_time=None,
257
+ total_calls=0,
258
+ success_count=0,
259
+ error_count=0,
260
+ total_chars_in=0,
261
+ total_chars_out=0,
262
+ total_duration_ms=0,
263
+ context_saved=0,
264
+ time_saved_ms=0,
265
+ tools=[],
266
+ )
267
+
268
+ # Separate run-level and tool-level records
269
+ run_records: list[dict[str, Any]] = []
270
+ tool_records_by_name: dict[str, list[dict[str, Any]]] = {}
271
+ timestamps: list[str] = []
272
+
273
+ for record in records:
274
+ record_type = record.get("type", "run")
275
+ ts = record.get("ts")
276
+ if ts:
277
+ timestamps.append(ts)
278
+
279
+ if record_type == "run":
280
+ run_records.append(record)
281
+ elif record_type == "tool":
282
+ tool_name = record.get("tool", "unknown")
283
+ if tool_name not in tool_records_by_name:
284
+ tool_records_by_name[tool_name] = []
285
+ tool_records_by_name[tool_name].append(record)
286
+
287
+ # Sort timestamps for range
288
+ timestamps.sort()
289
+
290
+ # Aggregate run-level stats
291
+ run_count = len(run_records)
292
+ run_success = sum(1 for r in run_records if r.get("success") is True)
293
+ run_error = run_count - run_success
294
+ total_chars_in = sum(int(r.get("chars_in", 0)) for r in run_records)
295
+ total_chars_out = sum(int(r.get("chars_out", 0)) for r in run_records)
296
+ run_duration = sum(int(r.get("duration_ms", 0)) for r in run_records)
297
+
298
+ # Aggregate per-tool stats
299
+ tool_stats: list[ToolStats] = []
300
+ total_tool_duration = 0
301
+
302
+ for tool_name, tool_records in sorted(tool_records_by_name.items()):
303
+ calls = len(tool_records)
304
+ success = sum(1 for r in tool_records if r.get("success") is True)
305
+ errors = calls - success
306
+ duration = sum(int(r.get("duration_ms", 0)) for r in tool_records)
307
+
308
+ tool_stats.append(
309
+ ToolStats(
310
+ tool=tool_name,
311
+ total_calls=calls,
312
+ success_count=success,
313
+ error_count=errors,
314
+ total_chars_in=0, # Tool records don't have chars
315
+ total_chars_out=0,
316
+ total_duration_ms=duration,
317
+ avg_duration_ms=duration / calls if calls > 0 else 0,
318
+ )
319
+ )
320
+
321
+ total_tool_duration += duration
322
+
323
+ # Calculate savings (context and time saved by consolidating run calls)
324
+ context_saved = run_count * self._context_per_call
325
+ time_saved = run_count * self._time_overhead_ms
326
+
327
+ # Calculate cost estimate (actual cost of tokens used)
328
+ input_tokens = total_chars_in / self._chars_per_token
329
+ output_tokens = total_chars_out / self._chars_per_token
330
+ cost_estimate = (
331
+ (input_tokens / 1_000_000) * self._cost_per_m_input
332
+ + (output_tokens / 1_000_000) * self._cost_per_m_output
333
+ )
334
+
335
+ # Calculate savings estimate (cost of context overhead avoided)
336
+ savings_usd = (context_saved / 1_000_000) * self._cost_per_m_input
337
+
338
+ return AggregatedStats(
339
+ period=period,
340
+ start_time=timestamps[0] if timestamps else None,
341
+ end_time=timestamps[-1] if timestamps else None,
342
+ total_calls=run_count,
343
+ success_count=run_success,
344
+ error_count=run_error,
345
+ total_chars_in=total_chars_in,
346
+ total_chars_out=total_chars_out,
347
+ total_duration_ms=run_duration,
348
+ context_saved=context_saved,
349
+ time_saved_ms=time_saved,
350
+ tools=tool_stats,
351
+ model=self._model,
352
+ cost_estimate_usd=cost_estimate,
353
+ savings_usd=savings_usd,
354
+ )
ot/stats/timing.py ADDED
@@ -0,0 +1,57 @@
1
+ """Timing context manager for tool call statistics.
2
+
3
+ Provides a reusable context manager that handles timing, success/error
4
+ tracking, and stats recording for tool calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from contextlib import contextmanager
11
+ from typing import TYPE_CHECKING
12
+
13
+ from ot.stats.jsonl_writer import get_client_name, record_tool_stats
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterator
17
+
18
+
19
+ @contextmanager
20
+ def timed_tool_call(tool_name: str, client: str | None = None) -> Iterator[None]:
21
+ """Context manager for timing tool calls and recording stats.
22
+
23
+ Measures execution time, tracks success/failure, and records stats
24
+ to the global stats writer.
25
+
26
+ Args:
27
+ tool_name: Fully qualified tool name (e.g., "brave.search")
28
+ client: MCP client name. If None, uses global client name.
29
+
30
+ Yields:
31
+ None
32
+
33
+ Example:
34
+ with timed_tool_call("brave.search"):
35
+ result = brave.search(query="test")
36
+ """
37
+ if client is None:
38
+ client = get_client_name()
39
+
40
+ start_time = time.monotonic()
41
+ error_type: str | None = None
42
+ success = True
43
+
44
+ try:
45
+ yield
46
+ except Exception as e:
47
+ success = False
48
+ error_type = type(e).__name__
49
+ raise
50
+ finally:
51
+ duration_ms = int((time.monotonic() - start_time) * 1000)
52
+ record_tool_stats(
53
+ tool=tool_name,
54
+ duration_ms=duration_ms,
55
+ success=success,
56
+ error_type=error_type,
57
+ )
ot/support.py ADDED
@@ -0,0 +1,63 @@
1
+ """Centralized support information for OneTool.
2
+
3
+ Single source of truth for donation/support links, messages, and version.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ # Support URLs
11
+ KOFI_URL = "https://ko-fi.com/beycom"
12
+ KOFI_HANDLE = "beycom"
13
+
14
+ # Support messages
15
+ SUPPORT_MESSAGE = "If you find OneTool useful, please consider supporting development!"
16
+ SUPPORT_MESSAGE_SHORT = "Support OneTool development"
17
+
18
+ # For HTML reports
19
+ SUPPORT_HTML_TITLE = "Support OneTool"
20
+ SUPPORT_HTML_MESSAGE = "If you find this project useful, please consider buying me a coffee!"
21
+ SUPPORT_HTML_BUTTON_TEXT = "Buy me a coffee on Ko-fi"
22
+
23
+
24
+ def get_support_dict() -> dict[str, str]:
25
+ """Get support info as a dictionary for JSON output.
26
+
27
+ Returns:
28
+ Dict with support URLs and messages
29
+ """
30
+ return {
31
+ "message": SUPPORT_MESSAGE,
32
+ "kofi_url": KOFI_URL,
33
+ }
34
+
35
+
36
+ def get_startup_message() -> str:
37
+ """Get support message for server startup logs.
38
+
39
+ Returns:
40
+ Formatted startup message with support link
41
+ """
42
+ return f"{SUPPORT_MESSAGE_SHORT}: {KOFI_URL}"
43
+
44
+
45
+ def get_support_banner() -> str:
46
+ """Get Rich-formatted support message for CLI banners.
47
+
48
+ Returns:
49
+ Rich markup string for console.print()
50
+ """
51
+ return f"[yellow]☕ Please buy me a coffee:[/yellow] [link={KOFI_URL}]{KOFI_URL}[/link]"
52
+
53
+
54
+ def get_version() -> str:
55
+ """Get OneTool package version.
56
+
57
+ Returns:
58
+ Version string, or "dev" if not installed as a package.
59
+ """
60
+ try:
61
+ return version("onetool")
62
+ except PackageNotFoundError:
63
+ return "dev"
ot/tools.py ADDED
@@ -0,0 +1,114 @@
1
+ """Inter-tool calling API for bundled and extension tools.
2
+
3
+ Provides functions for calling other tools programmatically:
4
+ - call_tool(): Call a tool by its full pack.function name
5
+ - get_pack(): Get a pack proxy for calling multiple functions
6
+
7
+ Example usage in an extension tool:
8
+
9
+ from ot.tools import call_tool, get_pack
10
+
11
+ # Call a single tool by name
12
+ result = call_tool("llm.transform", input=text, prompt="Summarize")
13
+
14
+ # Get a pack for multiple calls
15
+ brave = get_pack("brave")
16
+ results = brave.search(query="test")
17
+
18
+ Note: These functions are only available in bundled and extension tools.
19
+ Isolated tools (subprocess with PEP 723) cannot use this API.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any
25
+
26
+
27
+ def call_tool(name: str, **kwargs: Any) -> Any:
28
+ """Call another tool by its full name.
29
+
30
+ Args:
31
+ name: Full tool name with pack prefix (e.g., "llm.transform", "brave.search").
32
+ Must contain a dot separator.
33
+ **kwargs: Keyword arguments to pass to the tool function.
34
+
35
+ Returns:
36
+ The result from the tool function.
37
+
38
+ Raises:
39
+ ValueError: If name doesn't contain a dot separator.
40
+ KeyError: If the pack or function is not found.
41
+
42
+ Example:
43
+ result = call_tool("llm.transform", input="Hello", prompt="Translate to Spanish")
44
+ """
45
+ if "." not in name:
46
+ raise ValueError(
47
+ f"Tool name must include pack prefix (e.g., 'pack.function'), got: {name}"
48
+ )
49
+
50
+ from ot.executor.tool_loader import load_tool_registry
51
+
52
+ registry = load_tool_registry()
53
+
54
+ if name not in registry.functions:
55
+ pack_name, func_name = name.rsplit(".", 1)
56
+
57
+ if pack_name not in registry.packs:
58
+ available_packs = ", ".join(sorted(registry.packs.keys()))
59
+ raise KeyError(
60
+ f"Pack '{pack_name}' not found. Available packs: {available_packs}"
61
+ )
62
+
63
+ pack_funcs = registry.packs[pack_name]
64
+ if hasattr(pack_funcs, "__getattr__"):
65
+ # WorkerPackProxy - list functions differently
66
+ available_funcs = "use get_pack() to discover functions"
67
+ else:
68
+ available_funcs = ", ".join(sorted(pack_funcs.keys()))
69
+ raise KeyError(
70
+ f"Function '{func_name}' not found in pack '{pack_name}'. "
71
+ f"Available: {available_funcs}"
72
+ )
73
+
74
+ return registry.functions[name](**kwargs)
75
+
76
+
77
+ def get_pack(name: str) -> Any:
78
+ """Get a pack proxy for calling multiple functions.
79
+
80
+ Returns a proxy object that allows calling pack functions using dot notation.
81
+
82
+ Args:
83
+ name: Pack name (e.g., "brave", "llm", "file").
84
+
85
+ Returns:
86
+ Pack proxy object with tool functions as attributes.
87
+
88
+ Raises:
89
+ KeyError: If the pack is not found.
90
+
91
+ Example:
92
+ brave = get_pack("brave")
93
+ results = brave.search(query="python")
94
+
95
+ llm = get_pack("llm")
96
+ summary = llm.transform(input=text, prompt="Summarize")
97
+ """
98
+ from ot.executor.pack_proxy import build_execution_namespace
99
+ from ot.executor.tool_loader import load_tool_registry
100
+
101
+ registry = load_tool_registry()
102
+
103
+ if name not in registry.packs:
104
+ available = ", ".join(sorted(registry.packs.keys()))
105
+ raise KeyError(f"Pack '{name}' not found. Available packs: {available}")
106
+
107
+ # Build namespace to get the wrapped proxy with stats tracking
108
+ namespace = build_execution_namespace(registry)
109
+
110
+ if name in namespace:
111
+ return namespace[name]
112
+
113
+ # Fallback to raw pack (shouldn't happen normally)
114
+ return registry.packs[name]
ot/utils/__init__.py ADDED
@@ -0,0 +1,81 @@
1
+ """OneTool utilities.
2
+
3
+ Provides shared utilities for internal tools:
4
+ - Text processing: truncate, format_error, run_command
5
+ - Batch processing: batch_execute, normalize_items, format_batch_results
6
+ - Caching: cache (TTL-based memoization)
7
+ - HTTP utilities: safe_request, api_headers, check_api_key
8
+ - Dependencies: check_cli, check_lib, ensure_cli, ensure_lib
9
+ - Factory: lazy_client, LazyClient
10
+
11
+ Extension tools (user-created in .onetool/tools/) can import directly from
12
+ ot.* modules for logging, config, and inter-tool calling.
13
+ """
14
+
15
+ from ot.utils.batch import batch_execute, format_batch_results, normalize_items
16
+ from ot.utils.cache import CacheNamespace, cache
17
+ from ot.utils.deps import (
18
+ Dependency,
19
+ DepsCheckResult,
20
+ check_cli,
21
+ check_deps,
22
+ check_lib,
23
+ check_secret,
24
+ ensure_cli,
25
+ ensure_lib,
26
+ requires_cli,
27
+ requires_lib,
28
+ )
29
+ from ot.utils.exceptions import flatten_exception_group
30
+ from ot.utils.factory import LazyClient, lazy_client
31
+ from ot.utils.format import serialize_result
32
+ from ot.utils.http import api_headers, check_api_key, safe_request
33
+ from ot.utils.platform import get_install_hint
34
+ from ot.utils.sanitize import (
35
+ sanitize_output,
36
+ sanitize_tag_closes,
37
+ sanitize_triggers,
38
+ wrap_external_content,
39
+ )
40
+ from ot.utils.truncate import format_error, run_command, truncate
41
+
42
+ __all__ = [
43
+ # Cache
44
+ "CacheNamespace",
45
+ "Dependency",
46
+ # Dependencies
47
+ "DepsCheckResult",
48
+ # Factory
49
+ "LazyClient",
50
+ # HTTP
51
+ "api_headers",
52
+ # Batch processing
53
+ "batch_execute",
54
+ "cache",
55
+ "check_api_key",
56
+ "check_cli",
57
+ "check_deps",
58
+ "check_lib",
59
+ "check_secret",
60
+ "ensure_cli",
61
+ "ensure_lib",
62
+ # Existing utilities
63
+ "flatten_exception_group",
64
+ "format_batch_results",
65
+ # Truncate
66
+ "format_error",
67
+ "get_install_hint",
68
+ "lazy_client",
69
+ "normalize_items",
70
+ "requires_cli",
71
+ "requires_lib",
72
+ "run_command",
73
+ "safe_request",
74
+ # Sanitization
75
+ "sanitize_output",
76
+ "sanitize_tag_closes",
77
+ "sanitize_triggers",
78
+ "serialize_result",
79
+ "truncate",
80
+ "wrap_external_content",
81
+ ]