onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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 (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/meta.py CHANGED
@@ -102,6 +102,7 @@ __all__ = [
102
102
  "packs",
103
103
  "reload",
104
104
  "result",
105
+ "security",
105
106
  "snippets",
106
107
  "stats",
107
108
  "timed",
@@ -122,6 +123,45 @@ def version() -> str:
122
123
  return __version__
123
124
 
124
125
 
126
+ def security(*, check: str = "") -> dict[str, Any]:
127
+ """Check security rules for code validation.
128
+
129
+ OneTool uses an allowlist-based security model: everything is blocked
130
+ by default, and only explicitly allowed builtins, imports, and calls
131
+ are permitted. Tool namespaces (ot.*, brave.*, etc.) are auto-allowed.
132
+
133
+ Args:
134
+ check: Pattern to check (e.g., "os", "json.loads", "pickle.*").
135
+ If empty, returns a summary of all security rules.
136
+
137
+ Returns:
138
+ If check is provided: Dict with 'pattern', 'status' (allowed/blocked/warned),
139
+ 'category', and 'reason' explaining why.
140
+ If check is empty: Dict with summary of all security categories
141
+ (builtins, imports, calls, dunders, tool_namespaces).
142
+
143
+ Example:
144
+ ot.security() # Show all rules
145
+ ot.security(check="os") # "blocked: import"
146
+ ot.security(check="json") # "allowed: import"
147
+ ot.security(check="json.loads") # "allowed: module in imports"
148
+ ot.security(check="pickle.load") # "blocked: calls"
149
+ ot.security(check="brave.search") # "allowed: tool namespace"
150
+ """
151
+ from ot.executor.validator import get_security_status, get_security_summary
152
+
153
+ with log(span="ot.security", check=check or None) as s:
154
+ if check:
155
+ result = get_security_status(check)
156
+ s.add("status", result["status"])
157
+ s.add("category", result["category"])
158
+ return result
159
+ else:
160
+ summary = get_security_summary()
161
+ s.add("status", summary.get("status", "unknown"))
162
+ return summary
163
+
164
+
125
165
  def timed(func: _Callable[..., _T], **kwargs: Any) -> dict[str, Any]:
126
166
  """Execute a function and return result with timing info.
127
167
 
@@ -155,12 +195,14 @@ def get_ot_pack_functions() -> dict[str, Any]:
155
195
  return {
156
196
  "tools": tools,
157
197
  "packs": packs,
198
+ "servers": servers,
158
199
  "aliases": aliases,
159
200
  "snippets": snippets,
160
201
  "config": config,
161
202
  "health": health,
162
203
  "help": help,
163
204
  "result": result,
205
+ "security": security,
164
206
  "stats": stats,
165
207
  "notify": notify,
166
208
  "reload": reload,
@@ -178,7 +220,7 @@ def _get_doc_url(pack: str) -> str:
178
220
  """Get documentation URL for a pack.
179
221
 
180
222
  Args:
181
- pack: Pack name (e.g., "brave", "firecrawl")
223
+ pack: Pack name (e.g., "brave", "file")
182
224
 
183
225
  Returns:
184
226
  Documentation URL for the pack
@@ -227,7 +269,8 @@ def _format_general_help() -> str:
227
269
  ## Discovery
228
270
  ot.tools() - List all tools
229
271
  ot.tools(pattern="web") - Filter by pattern
230
- ot.packs() - List all packs
272
+ ot.packs() - List all packs (local + MCP)
273
+ ot.servers() - List MCP proxy servers
231
274
  ot.snippets() - List all snippets
232
275
  ot.aliases() - List all aliases
233
276
  ot.help(query="..") - Search for help
@@ -235,7 +278,7 @@ def _format_general_help() -> str:
235
278
  ## Info Levels
236
279
  info="list" - Names only
237
280
  info="min" - Name + description (default)
238
- info="full" - Everything
281
+ info="full" - Everything (includes instructions)
239
282
 
240
283
  ## Quick Examples
241
284
  brave.search(query="AI news")
@@ -448,7 +491,8 @@ def _format_search_results(
448
491
  lines.append("")
449
492
  lines.append("Try browsing with:")
450
493
  lines.append(" ot.tools() - List all tools")
451
- lines.append(" ot.packs() - List all packs")
494
+ lines.append(" ot.packs() - List all packs (local + MCP)")
495
+ lines.append(" ot.servers() - List MCP proxy servers")
452
496
  lines.append(" ot.snippets() - List all snippets")
453
497
  lines.append(" ot.aliases() - List all aliases")
454
498
 
@@ -504,7 +548,7 @@ def _build_tool_info(
504
548
  Args:
505
549
  full_name: Full tool name (e.g., "brave.search")
506
550
  func: The function object
507
- source: Source identifier (e.g., "local", "proxy:github")
551
+ source: Source identifier (e.g., "local", "mcp:github")
508
552
  info: Output verbosity level ("list", "min", "full")
509
553
 
510
554
  Returns:
@@ -577,7 +621,20 @@ def _schema_to_signature(full_name: str, schema: dict[str, Any]) -> str:
577
621
  "array": "list",
578
622
  "object": "dict",
579
623
  }
580
- py_type = type_map.get(prop_type, prop_type)
624
+
625
+ # Handle JSON Schema union types (e.g., ["string", "null"])
626
+ if isinstance(prop_type, list):
627
+ # Filter out "null" and map remaining types
628
+ non_null = [t for t in prop_type if t != "null"]
629
+ if non_null:
630
+ mapped = [type_map.get(t, t) for t in non_null]
631
+ py_type = " | ".join(mapped)
632
+ if "null" in prop_type:
633
+ py_type = f"{py_type} | None"
634
+ else:
635
+ py_type = "None"
636
+ else:
637
+ py_type = type_map.get(prop_type, prop_type)
581
638
 
582
639
  if prop_name in required:
583
640
  params.append(f"{prop_name}: {py_type}")
@@ -626,7 +683,7 @@ def _build_proxy_tool_info(
626
683
  full_name: Full tool name (e.g., "github.search")
627
684
  description: Tool description from MCP server
628
685
  input_schema: JSON Schema for tool input
629
- source: Source identifier (e.g., "proxy:github")
686
+ source: Source identifier (e.g., "mcp:github")
630
687
  info: Output verbosity level ("list", "min", "full")
631
688
 
632
689
  Returns:
@@ -718,7 +775,7 @@ def tools(
718
775
  tool_name,
719
776
  proxy_tool.description or "",
720
777
  proxy_tool.input_schema,
721
- f"proxy:{proxy_tool.server}",
778
+ f"mcp:{proxy_tool.server}",
722
779
  info,
723
780
  )
724
781
  )
@@ -777,22 +834,44 @@ def packs(
777
834
  # info="full" - detailed info for each matching pack
778
835
  if info == "full":
779
836
  results: list[dict[str, Any] | str] = []
837
+ cfg = get_config()
838
+
780
839
  for pack_name in all_pack_names:
781
840
  is_local = pack_name in local_packs
782
841
 
783
842
  # Build detailed pack info
784
843
  lines = [f"# {pack_name} pack", ""]
785
844
 
786
- # Get instructions
845
+ # Show source type
846
+ if is_local:
847
+ lines.append("**Type:** Local")
848
+ else:
849
+ lines.append("**Type:** MCP Proxy Server")
850
+ lines.append("")
851
+
852
+ # Get instructions from prompts.yaml
787
853
  try:
788
854
  prompts_config = get_prompts()
789
855
  configured = get_pack_instructions(prompts_config, pack_name)
790
856
  if configured:
857
+ lines.append("## Instructions")
858
+ lines.append("")
791
859
  lines.append(configured)
792
860
  lines.append("")
793
861
  except PromptsError:
794
862
  pass
795
863
 
864
+ # For proxy packs, also check server config for instructions
865
+ if not is_local and pack_name in cfg.servers:
866
+ server_cfg = cfg.servers[pack_name]
867
+ if server_cfg.instructions:
868
+ # Only add header if not already added from prompts
869
+ if "## Instructions" not in "\n".join(lines):
870
+ lines.append("## Instructions")
871
+ lines.append("")
872
+ lines.append(server_cfg.instructions.strip())
873
+ lines.append("")
874
+
796
875
  # List tools in this pack
797
876
  lines.append("## Tools")
798
877
  lines.append("")
@@ -827,7 +906,7 @@ def packs(
827
906
 
828
907
  for pack_name in all_pack_names:
829
908
  is_local = pack_name in local_packs
830
- source = "local" if is_local else "proxy"
909
+ source = "local" if is_local else "mcp"
831
910
 
832
911
  # Count tools in pack
833
912
  if is_local:
@@ -851,6 +930,127 @@ def packs(
851
930
  return packs_list
852
931
 
853
932
 
933
+ def servers(
934
+ *,
935
+ pattern: str = "",
936
+ info: InfoLevel = "min",
937
+ ) -> list[dict[str, Any] | str]:
938
+ """List configured MCP proxy servers with optional filtering.
939
+
940
+ Shows all MCP servers configured in servers.yaml, including their
941
+ connection status, tool count, and instructions.
942
+
943
+ Args:
944
+ pattern: Filter servers by name pattern (case-insensitive substring)
945
+ info: Output verbosity level - "list" (names only), "min" (name + status + tool_count),
946
+ or "full" (detailed info with instructions and tools)
947
+
948
+ Returns:
949
+ List of server names (info="list") or server dicts/strings (info="min"/"full")
950
+
951
+ Example:
952
+ ot.servers()
953
+ ot.servers(pattern="github")
954
+ ot.servers(info="full")
955
+ ot.servers(pattern="devtools", info="full")
956
+ """
957
+ proxy = get_proxy_manager()
958
+ cfg = get_config()
959
+
960
+ with log(span="ot.servers", pattern=pattern or None, info=info) as s:
961
+ # Get all configured servers
962
+ all_server_names = sorted(cfg.servers.keys())
963
+
964
+ # Filter by pattern
965
+ if pattern:
966
+ all_server_names = [
967
+ name for name in all_server_names if pattern.lower() in name.lower()
968
+ ]
969
+
970
+ # info="list" - just names
971
+ if info == "list":
972
+ s.add("count", len(all_server_names))
973
+ return all_server_names # type: ignore[return-value]
974
+
975
+ # info="full" - detailed info for each server
976
+ if info == "full":
977
+ results: list[dict[str, Any] | str] = []
978
+
979
+ for server_name in all_server_names:
980
+ server_cfg = cfg.servers[server_name]
981
+ conn = proxy.get_connection(server_name)
982
+ status = "connected" if conn else "disconnected"
983
+ tool_count = len(proxy.list_tools(server=server_name)) if conn else 0
984
+
985
+ lines = [f"# {server_name} server", ""]
986
+ lines.append(f"**Type:** MCP Proxy Server ({server_cfg.type})")
987
+ lines.append(f"**Status:** {status}")
988
+ lines.append(f"**Enabled:** {server_cfg.enabled}")
989
+ if server_cfg.type == "http" and server_cfg.url:
990
+ lines.append(f"**URL:** {server_cfg.url}")
991
+ elif server_cfg.type == "stdio" and server_cfg.command:
992
+ cmd = f"{server_cfg.command} {' '.join(server_cfg.args)}"
993
+ lines.append(f"**Command:** {cmd}")
994
+ lines.append("")
995
+
996
+ # Show instructions if configured
997
+ if server_cfg.instructions:
998
+ lines.append("## Instructions")
999
+ lines.append("")
1000
+ lines.append(server_cfg.instructions.strip())
1001
+ lines.append("")
1002
+
1003
+ # List tools if connected
1004
+ if conn:
1005
+ lines.append(f"## Tools ({tool_count})")
1006
+ lines.append("")
1007
+ proxy_tools = proxy.list_tools(server=server_name)
1008
+ for tool in sorted(proxy_tools, key=lambda t: t.name):
1009
+ desc = tool.description or "(no description)"
1010
+ first_line = desc.split("\n")[0].strip()
1011
+ lines.append(f"- **{server_name}.{tool.name}**: {first_line}")
1012
+ elif server_cfg.enabled:
1013
+ lines.append("## Tools")
1014
+ lines.append("")
1015
+ lines.append("(not connected)")
1016
+ # Show error if available
1017
+ error = proxy.get_error(server_name)
1018
+ if error:
1019
+ lines.append("")
1020
+ lines.append(f"**Error:** {error}")
1021
+
1022
+ results.append("\n".join(lines))
1023
+
1024
+ s.add("count", len(results))
1025
+ return results
1026
+
1027
+ # info="min" (default) - summary for each server
1028
+ servers_list: list[dict[str, Any] | str] = []
1029
+
1030
+ for server_name in all_server_names:
1031
+ server_cfg = cfg.servers[server_name]
1032
+ conn = proxy.get_connection(server_name)
1033
+ status = "connected" if conn else "disconnected"
1034
+ tool_count = len(proxy.list_tools(server=server_name)) if conn else 0
1035
+
1036
+ server_info: dict[str, Any] = {
1037
+ "name": server_name,
1038
+ "type": server_cfg.type,
1039
+ "enabled": server_cfg.enabled,
1040
+ "status": status,
1041
+ "tool_count": tool_count,
1042
+ }
1043
+ # Include error if disconnected
1044
+ if not conn:
1045
+ error = proxy.get_error(server_name)
1046
+ if error:
1047
+ server_info["error"] = error
1048
+ servers_list.append(server_info)
1049
+
1050
+ s.add("count", len(servers_list))
1051
+ return servers_list
1052
+
1053
+
854
1054
  # ============================================================================
855
1055
  # Messaging Functions
856
1056
  # ============================================================================
@@ -1035,14 +1235,21 @@ def health() -> dict[str, Any]:
1035
1235
  "tool_count": tool_count,
1036
1236
  }
1037
1237
 
1038
- server_statuses: dict[str, str] = {}
1238
+ server_statuses: dict[str, Any] = {}
1039
1239
  for server_name in cfg.servers:
1040
1240
  conn = proxy.get_connection(server_name)
1041
- server_statuses[server_name] = "connected" if conn else "disconnected"
1241
+ if conn:
1242
+ server_statuses[server_name] = "connected"
1243
+ else:
1244
+ error = proxy.get_error(server_name)
1245
+ server_statuses[server_name] = {"status": "disconnected", "error": error} if error else "disconnected"
1042
1246
 
1043
1247
  proxy_status: dict[str, Any] = {
1044
1248
  "status": "ok"
1045
- if all(status == "connected" for status in server_statuses.values())
1249
+ if all(
1250
+ (s == "connected" if isinstance(s, str) else s.get("status") == "connected")
1251
+ for s in server_statuses.values()
1252
+ )
1046
1253
  or not server_statuses
1047
1254
  else "degraded",
1048
1255
  "server_count": len(cfg.servers),
@@ -1074,6 +1281,7 @@ def reload() -> str:
1074
1281
  - Prompts
1075
1282
  - MCP proxy connections
1076
1283
  - Parameter resolution caches
1284
+ - Security validation caches
1077
1285
 
1078
1286
  Use after modifying config files, adding/removing tools, or
1079
1287
  changing secrets during a session.
@@ -1121,6 +1329,11 @@ def reload() -> str:
1121
1329
  ot.executor.param_resolver.get_tool_param_names.cache_clear()
1122
1330
  ot.executor.param_resolver._mcp_param_cache.clear()
1123
1331
 
1332
+ # Clear security validator caches (depends on config and registry)
1333
+ import ot.executor.validator
1334
+ ot.executor.validator._get_tool_namespaces.cache_clear()
1335
+ ot.executor.validator._get_security_config.cache_clear()
1336
+
1124
1337
  # Reload config to validate and report stats
1125
1338
  cfg = get_config()
1126
1339
 
@@ -1471,7 +1684,7 @@ def help(*, query: str = "", info: InfoLevel = "min") -> str:
1471
1684
  Example:
1472
1685
  ot.help()
1473
1686
  ot.help(query="brave.search")
1474
- ot.help(query="firecrawl")
1687
+ ot.help(query="brave")
1475
1688
  ot.help(query="$b_q")
1476
1689
  ot.help(query="web fetch", info="list")
1477
1690
  """
@@ -1494,6 +1707,15 @@ def help(*, query: str = "", info: InfoLevel = "min") -> str:
1494
1707
  s.add("match", query)
1495
1708
  return _format_tool_help(tool, pack)
1496
1709
 
1710
+ # Check for exact server match (MCP proxy servers)
1711
+ server_names = servers(info="list")
1712
+ if query in server_names:
1713
+ server_results = servers(pattern=query, info="full")
1714
+ if server_results:
1715
+ s.add("type", "server")
1716
+ s.add("match", query)
1717
+ return str(server_results[0])
1718
+
1497
1719
  # Check for exact pack match
1498
1720
  pack_names = packs(info="list")
1499
1721
  if query in pack_names:
ot/paths.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """Path resolution for OneTool global and project directories.
2
2
 
3
- OneTool uses a three-tier directory structure:
4
- - Bundled: package data in ot.config.defaults — read-only defaults
3
+ OneTool uses a two-tier directory structure:
5
4
  - Global: ~/.onetool/ — user-wide settings, secrets
6
5
  - Project: .onetool/ — project-specific config
7
6
 
@@ -9,10 +8,10 @@ Each .onetool/ directory uses subdirectories to organise files by purpose:
9
8
  - config/ — YAML configuration files
10
9
  - logs/ — Application log files
11
10
  - stats/ — Statistics data (stats.jsonl)
12
- - sessions/ — Browser session state
13
11
  - tools/ — Reserved for installed tool packs
14
12
 
15
13
  Directories are created lazily on first use, not on install.
14
+ Templates in ot.config.global_templates are copied to ~/.onetool/ on init.
16
15
  """
17
16
 
18
17
  from __future__ import annotations
@@ -30,13 +29,9 @@ PROJECT_DIR_NAME = ".onetool"
30
29
  CONFIG_SUBDIR = "config"
31
30
  LOGS_SUBDIR = "logs"
32
31
  STATS_SUBDIR = "stats"
33
- SESSIONS_SUBDIR = "sessions"
34
32
  TOOLS_SUBDIR = "tools"
35
33
 
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)
34
+ # Package containing global templates (copied to ~/.onetool/ on init)
40
35
  GLOBAL_TEMPLATES_PACKAGE = "ot.config.global_templates"
41
36
 
42
37
 
@@ -49,7 +44,7 @@ def _resolve_package_dir(package_name: str, description: str) -> Path:
49
44
  - Development mode
50
45
 
51
46
  Args:
52
- package_name: Dotted package name (e.g., "ot.config.defaults")
47
+ package_name: Dotted package name (e.g., "ot.config.global_templates")
53
48
  description: Human-readable description for error messages
54
49
 
55
50
  Returns:
@@ -105,29 +100,13 @@ def _resolve_package_dir(package_name: str, description: str) -> Path:
105
100
  )
106
101
 
107
102
 
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
103
  def get_global_templates_dir() -> Path:
126
104
  """Get the global templates directory path.
127
105
 
128
106
  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.
107
+ copied to ~/.onetool/ on init. These provide documentation and examples
108
+ for configuration. Also contains subdirectories like diagram-templates/
109
+ and tool_templates/ for tool-specific resources.
131
110
 
132
111
  Returns:
133
112
  Path to global templates directory (read-only package data)
@@ -156,9 +135,14 @@ def get_effective_cwd() -> Path:
156
135
  def get_global_dir() -> Path:
157
136
  """Get the global OneTool directory path.
158
137
 
138
+ Can be overridden via OT_GLOBAL_DIR environment variable.
139
+
159
140
  Returns:
160
- Path to ~/.onetool/ (not necessarily existing)
141
+ Path to ~/.onetool/ or OT_GLOBAL_DIR if set (not necessarily existing)
161
142
  """
143
+ env_global = os.getenv("OT_GLOBAL_DIR")
144
+ if env_global:
145
+ return Path(env_global).resolve()
162
146
  return Path.home() / GLOBAL_DIR_NAME
163
147
 
164
148
 
@@ -232,11 +216,9 @@ def create_backup(file_path: Path) -> Path:
232
216
  def ensure_global_dir(quiet: bool = False, force: bool = False) -> Path:
233
217
  """Ensure the global OneTool directory exists with subdirectory structure.
234
218
 
235
- Creates ~/.onetool/ with subdirectories (config/, logs/, stats/, sessions/, tools/)
219
+ Creates ~/.onetool/ with subdirectories (config/, logs/, stats/, tools/)
236
220
  and copies template config files from global_templates to config/.
237
221
  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
222
 
241
223
  Args:
242
224
  quiet: Suppress creation messages
@@ -255,12 +237,12 @@ def ensure_global_dir(quiet: bool = False, force: bool = False) -> Path:
255
237
 
256
238
  # Create directory structure with subdirectories
257
239
  global_dir.mkdir(parents=True, exist_ok=True)
258
- subdirs = [CONFIG_SUBDIR, LOGS_SUBDIR, STATS_SUBDIR, SESSIONS_SUBDIR, TOOLS_SUBDIR]
240
+ subdirs = [CONFIG_SUBDIR, LOGS_SUBDIR, STATS_SUBDIR, TOOLS_SUBDIR]
259
241
  for subdir in subdirs:
260
242
  (global_dir / subdir).mkdir(exist_ok=True)
261
243
 
262
244
  # Copy template config files to config/ subdirectory
263
- # Only YAML files are copied; subdirectories stay in bundled defaults
245
+ # Only YAML files are copied from global_templates/
264
246
  # Files named *-template.yaml are copied without the -template suffix
265
247
  # (to avoid gitignore patterns on secrets.yaml)
266
248
  config_dir = global_dir / CONFIG_SUBDIR
@@ -275,6 +257,20 @@ def ensure_global_dir(quiet: bool = False, force: bool = False) -> Path:
275
257
  if not dest.exists() or force:
276
258
  shutil.copy(config_file, dest)
277
259
  copied_items.append(f"config/{dest_name}")
260
+
261
+ # Copy resource subdirectories (e.g., diagram-templates/)
262
+ # These contain user-customizable template files
263
+ for template_subdir in templates_dir.iterdir():
264
+ if template_subdir.is_dir() and not template_subdir.name.startswith("_"):
265
+ # Skip tool_templates - those are code templates accessed via get_global_templates_dir()
266
+ if template_subdir.name == "tool_templates":
267
+ continue
268
+ dest_subdir = config_dir / template_subdir.name
269
+ if not dest_subdir.exists() or force:
270
+ if dest_subdir.exists():
271
+ shutil.rmtree(dest_subdir)
272
+ shutil.copytree(template_subdir, dest_subdir)
273
+ copied_items.append(f"config/{template_subdir.name}/")
278
274
  except FileNotFoundError:
279
275
  # Global templates not available (dev environment without package install)
280
276
  pass
@@ -295,7 +291,7 @@ def ensure_project_dir(path: Path | None = None, quiet: bool = False) -> Path:
295
291
  """Ensure the project OneTool directory exists with subdirectory structure.
296
292
 
297
293
  Creates .onetool/ in the specified directory or effective cwd,
298
- including subdirectories (config/, logs/, stats/, sessions/, tools/).
294
+ including subdirectories (config/, logs/, stats/, tools/).
299
295
 
300
296
  Args:
301
297
  path: Project root (default: get_effective_cwd())
@@ -312,7 +308,7 @@ def ensure_project_dir(path: Path | None = None, quiet: bool = False) -> Path:
312
308
 
313
309
  # Create directory structure with subdirectories
314
310
  project_dir.mkdir(parents=True, exist_ok=True)
315
- subdirs = [CONFIG_SUBDIR, LOGS_SUBDIR, STATS_SUBDIR, SESSIONS_SUBDIR, TOOLS_SUBDIR]
311
+ subdirs = [CONFIG_SUBDIR, LOGS_SUBDIR, STATS_SUBDIR, TOOLS_SUBDIR]
316
312
  for subdir in subdirs:
317
313
  (project_dir / subdir).mkdir(exist_ok=True)
318
314
 
@@ -409,19 +405,6 @@ def get_stats_dir(base_dir: Path | None = None) -> Path:
409
405
  return base / STATS_SUBDIR
410
406
 
411
407
 
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
408
  def resolve_cwd_path(path: str) -> Path:
426
409
  """Resolve a path relative to the project working directory (OT_CWD).
427
410