pipulate 2.45__tar.gz → 2.47__tar.gz

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 (89) hide show
  1. {pipulate-2.45/pipulate.egg-info → pipulate-2.47}/PKG-INFO +1 -1
  2. {pipulate-2.45 → pipulate-2.47}/__init__.py +2 -2
  3. {pipulate-2.45 → pipulate-2.47}/cli.py +31 -8
  4. {pipulate-2.45 → pipulate-2.47/pipulate.egg-info}/PKG-INFO +1 -1
  5. {pipulate-2.45 → pipulate-2.47}/pipulate.egg-info/SOURCES.txt +1 -0
  6. {pipulate-2.45 → pipulate-2.47}/pyproject.toml +1 -1
  7. {pipulate-2.45 → pipulate-2.47}/server.py +0 -1
  8. {pipulate-2.45 → pipulate-2.47}/tools/__init__.py +32 -0
  9. pipulate-2.47/tools/connector_tools.py +98 -0
  10. {pipulate-2.45 → pipulate-2.47}/LICENSE +0 -0
  11. {pipulate-2.45 → pipulate-2.47}/MANIFEST.in +0 -0
  12. {pipulate-2.45 → pipulate-2.47}/README.md +0 -0
  13. {pipulate-2.45 → pipulate-2.47}/apps/010_introduction.py +0 -0
  14. {pipulate-2.45 → pipulate-2.47}/apps/015_config.py +0 -0
  15. {pipulate-2.45 → pipulate-2.47}/apps/020_profiles.py +0 -0
  16. {pipulate-2.45 → pipulate-2.47}/apps/025_aspect.py +0 -0
  17. {pipulate-2.45 → pipulate-2.47}/apps/030_roles.py +0 -0
  18. {pipulate-2.45 → pipulate-2.47}/apps/040_hello_workflow.py +0 -0
  19. {pipulate-2.45 → pipulate-2.47}/apps/050_documentation.py +0 -0
  20. {pipulate-2.45 → pipulate-2.47}/apps/060_tasks.py +0 -0
  21. {pipulate-2.45 → pipulate-2.47}/apps/070_history.py +0 -0
  22. {pipulate-2.45 → pipulate-2.47}/apps/110_parameter_buster.py +0 -0
  23. {pipulate-2.45 → pipulate-2.47}/apps/120_link_graph.py +0 -0
  24. {pipulate-2.45 → pipulate-2.47}/apps/130_gap_analysis.py +0 -0
  25. {pipulate-2.45 → pipulate-2.47}/apps/200_workflow_genesis.py +0 -0
  26. {pipulate-2.45 → pipulate-2.47}/apps/210_widget_examples.py +0 -0
  27. {pipulate-2.45 → pipulate-2.47}/apps/220_roadmap.py +0 -0
  28. {pipulate-2.45 → pipulate-2.47}/apps/230_dev_assistant.py +0 -0
  29. {pipulate-2.45 → pipulate-2.47}/apps/240_simon_mcp.py +0 -0
  30. {pipulate-2.45 → pipulate-2.47}/apps/300_blank_placeholder.py +0 -0
  31. {pipulate-2.45 → pipulate-2.47}/apps/400_botify_trifecta.py +0 -0
  32. {pipulate-2.45 → pipulate-2.47}/apps/440_browser_automation.py +0 -0
  33. {pipulate-2.45 → pipulate-2.47}/apps/450_stream_simulator.py +0 -0
  34. {pipulate-2.45 → pipulate-2.47}/apps/510_text_field.py +0 -0
  35. {pipulate-2.45 → pipulate-2.47}/apps/520_text_area.py +0 -0
  36. {pipulate-2.45 → pipulate-2.47}/apps/530_dropdown.py +0 -0
  37. {pipulate-2.45 → pipulate-2.47}/apps/540_checkboxes.py +0 -0
  38. {pipulate-2.45 → pipulate-2.47}/apps/550_radios.py +0 -0
  39. {pipulate-2.45 → pipulate-2.47}/apps/560_range.py +0 -0
  40. {pipulate-2.45 → pipulate-2.47}/apps/570_switch.py +0 -0
  41. {pipulate-2.45 → pipulate-2.47}/apps/580_upload.py +0 -0
  42. {pipulate-2.45 → pipulate-2.47}/apps/610_markdown.py +0 -0
  43. {pipulate-2.45 → pipulate-2.47}/apps/620_mermaid.py +0 -0
  44. {pipulate-2.45 → pipulate-2.47}/apps/630_prism.py +0 -0
  45. {pipulate-2.45 → pipulate-2.47}/apps/640_javascript.py +0 -0
  46. {pipulate-2.45 → pipulate-2.47}/apps/710_pandas.py +0 -0
  47. {pipulate-2.45 → pipulate-2.47}/apps/720_rich.py +0 -0
  48. {pipulate-2.45 → pipulate-2.47}/apps/730_matplotlib.py +0 -0
  49. {pipulate-2.45 → pipulate-2.47}/apps/810_webbrowser.py +0 -0
  50. {pipulate-2.45 → pipulate-2.47}/apps/820_selenium.py +0 -0
  51. {pipulate-2.45 → pipulate-2.47}/apps/830_pico_slider.py +0 -0
  52. {pipulate-2.45 → pipulate-2.47}/config.py +0 -0
  53. {pipulate-2.45 → pipulate-2.47}/imports/__init__.py +0 -0
  54. {pipulate-2.45 → pipulate-2.47}/imports/ai_dictdb.py +0 -0
  55. {pipulate-2.45 → pipulate-2.47}/imports/ai_tool_discovery_simple_parser.py +0 -0
  56. {pipulate-2.45 → pipulate-2.47}/imports/append_only_conversation.py +0 -0
  57. {pipulate-2.45 → pipulate-2.47}/imports/ascii_displays.py +0 -0
  58. {pipulate-2.45 → pipulate-2.47}/imports/botify/__init__.py +0 -0
  59. {pipulate-2.45 → pipulate-2.47}/imports/botify/code_generators.py +0 -0
  60. {pipulate-2.45 → pipulate-2.47}/imports/botify/true_schema_discoverer.py +0 -0
  61. {pipulate-2.45 → pipulate-2.47}/imports/botify_code_generation.py +0 -0
  62. {pipulate-2.45 → pipulate-2.47}/imports/crud.py +0 -0
  63. {pipulate-2.45 → pipulate-2.47}/imports/database_safety_wrapper.py +0 -0
  64. {pipulate-2.45 → pipulate-2.47}/imports/dom_processing/__init__.py +0 -0
  65. {pipulate-2.45 → pipulate-2.47}/imports/dom_processing/ai_dom_beautifier.py +0 -0
  66. {pipulate-2.45 → pipulate-2.47}/imports/dom_processing/enhanced_dom_processor.py +0 -0
  67. {pipulate-2.45 → pipulate-2.47}/imports/durable_backup_system.py +0 -0
  68. {pipulate-2.45 → pipulate-2.47}/imports/mcp_orchestrator.py +0 -0
  69. {pipulate-2.45 → pipulate-2.47}/imports/server_logging.py +0 -0
  70. {pipulate-2.45 → pipulate-2.47}/imports/stream_orchestrator.py +0 -0
  71. {pipulate-2.45 → pipulate-2.47}/imports/voice_synthesis.py +0 -0
  72. {pipulate-2.45 → pipulate-2.47}/pipulate/__init__.py +0 -0
  73. {pipulate-2.45 → pipulate-2.47}/pipulate/core.py +0 -0
  74. {pipulate-2.45 → pipulate-2.47}/pipulate.egg-info/dependency_links.txt +0 -0
  75. {pipulate-2.45 → pipulate-2.47}/pipulate.egg-info/entry_points.txt +0 -0
  76. {pipulate-2.45 → pipulate-2.47}/pipulate.egg-info/requires.txt +0 -0
  77. {pipulate-2.45 → pipulate-2.47}/pipulate.egg-info/top_level.txt +0 -0
  78. {pipulate-2.45 → pipulate-2.47}/setup.cfg +0 -0
  79. {pipulate-2.45 → pipulate-2.47}/tests/test_apply_airlock.py +0 -0
  80. {pipulate-2.45 → pipulate-2.47}/tests/test_mck_rep2.py +0 -0
  81. {pipulate-2.45 → pipulate-2.47}/tools/advanced_automation_tools.py +0 -0
  82. {pipulate-2.45 → pipulate-2.47}/tools/botify_tools.py +0 -0
  83. {pipulate-2.45 → pipulate-2.47}/tools/conversation_tools.py +0 -0
  84. {pipulate-2.45 → pipulate-2.47}/tools/dom_tools.py +0 -0
  85. {pipulate-2.45 → pipulate-2.47}/tools/keychain_tools.py +0 -0
  86. {pipulate-2.45 → pipulate-2.47}/tools/llm_optics.py +0 -0
  87. {pipulate-2.45 → pipulate-2.47}/tools/mcp_tools.py +0 -0
  88. {pipulate-2.45 → pipulate-2.47}/tools/scraper_tools.py +0 -0
  89. {pipulate-2.45 → pipulate-2.47}/tools/system_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 2.45
3
+ Version: 2.47
4
4
  Summary: AI-readiness for the agentic web — local-first, Nix-reproducible workflows. The successor to AI SEO software.
5
5
  Author-email: Mike Levin <pipulate@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -12,7 +12,7 @@ Usage:
12
12
  pipulate
13
13
  """
14
14
 
15
- __version__ = "2.45"
15
+ __version__ = "2.47"
16
16
  # APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
17
17
  # flake.nix's descMatch regex, whose character class excluded ' from the
18
18
  # CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
@@ -23,7 +23,7 @@ __version__ = "2.45"
23
23
  # is not a property of the system; the regex is. Blast radius is one banner:
24
24
  # nothing but flake.nix reads this name -- version_sync.py syncs __version__
25
25
  # and __description__, never this. So'wI' chu' -- "engage the cloaking device."
26
- __version_description__ = "Shoshinification"
26
+ __version_description__ = "Shoshin Kata Begins"
27
27
  # SPDX expression, single source of truth, synced into pyproject.toml by
28
28
  # scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
29
29
  # deprecated SPDX) because the header below grants "any later version".
@@ -109,7 +109,13 @@ def discover_tools(show_all=False, tool_name=None):
109
109
  border_style="cyan"
110
110
  ))
111
111
 
112
- for i, tool in enumerate(essential_tools, 1):
112
+ # The Rule of 7 is a string-literal list, not a registry read, so
113
+ # a denied tool would still be NAMED here and a model told to run
114
+ # it would waste a call on "not found". Filter without importing
115
+ # the registry (that import costs seconds); denied_tools() is cheap.
116
+ from tools import denied_tools
117
+ shown = [tool for tool in essential_tools if tool not in denied_tools()]
118
+ for i, tool in enumerate(shown, 1):
113
119
  console.print(f" {i}. [bold cyan]{tool}[/bold cyan]")
114
120
 
115
121
  console.print(f"\n[italic]Use `.venv/bin/python cli.py mcp-discover --all` to see all available tools.[/italic]")
@@ -133,9 +139,17 @@ def discover_tools(show_all=False, tool_name=None):
133
139
  console.print(f"❌ [bold red]Error running discovery:[/bold red] {e}")
134
140
  sys.exit(1)
135
141
 
136
- async def call_mcp_tool(tool_name: str, tool_args: dict):
137
- """Execute an MCP tool with the given arguments."""
138
- console.print(Panel(f"🔧 [bold cyan]Executing MCP Tool: {tool_name}[/bold cyan]", border_style="cyan"))
142
+ async def call_mcp_tool(tool_name: str, tool_args: dict, raw: bool = False):
143
+ """Execute an MCP tool with the given arguments.
144
+
145
+ raw=True prints the tool's returned dict as JSON and nothing else: no
146
+ panel, no Rich table. Added 2026-08-30 for the two-arm experiment, where
147
+ the registry arm must see what a tools/call would actually return rather
148
+ than an 80-column table cell that wraps line-oriented stdout. In raw mode
149
+ the exit code carries the tool's own success field.
150
+ """
151
+ if not raw:
152
+ console.print(Panel(f"🔧 [bold cyan]Executing MCP Tool: {tool_name}[/bold cyan]", border_style="cyan"))
139
153
 
140
154
  try:
141
155
  # Import MCP tools module
@@ -149,10 +163,14 @@ async def call_mcp_tool(tool_name: str, tool_args: dict):
149
163
 
150
164
  # Execute the tool
151
165
  tool_handler = registry[tool_name]
152
- console.print(f"⚡ Executing '{tool_name}' with args: {tool_args}")
166
+ if not raw:
167
+ console.print(f"⚡ Executing '{tool_name}' with args: {tool_args}")
153
168
 
154
169
  result = await tool_handler(tool_args)
155
170
 
171
+ if raw:
172
+ print(json.dumps(result, indent=2, default=str))
173
+ return bool(result.get("success", True)) if isinstance(result, dict) else True
156
174
  # Display results
157
175
  console.print(f"✅ [bold green]Tool execution complete![/bold green]")
158
176
 
@@ -506,9 +524,13 @@ def main():
506
524
  help='🎯 GOLDEN PATH: A JSON string containing all tool arguments. '
507
525
  'Use this for complex parameters to ensure perfect data transmission.')
508
526
 
527
+ call_parser.add_argument('--raw', action='store_true',
528
+ help='Print the tool result as JSON and nothing else; '
529
+ 'the exit code carries the tool\'s success field.')
509
530
  args = parser.parse_args()
510
531
 
511
- console.print(Panel("🚀 [bold cyan]Pipulate :: The Local-First AI-Readiness & Automation Workshop[/bold cyan] 🚀", border_style="cyan"))
532
+ if not getattr(args, 'raw', False):
533
+ console.print(Panel("🚀 [bold cyan]Pipulate :: The Local-First AI-Readiness & Automation Workshop[/bold cyan] 🚀", border_style="cyan"))
512
534
 
513
535
  if args.command == 'install':
514
536
  if not check_nix_installed():
@@ -556,7 +578,8 @@ def main():
556
578
  if args.json_args:
557
579
  try:
558
580
  params = json.loads(args.json_args)
559
- console.print(f"🎯 [bold green]Golden Path: Using JSON arguments[/bold green]")
581
+ if not args.raw:
582
+ console.print(f"🎯 [bold green]Golden Path: Using JSON arguments[/bold green]")
560
583
  except json.JSONDecodeError as e:
561
584
  console.print(f"❌ [bold red]Error: Invalid JSON provided to --json-args.[/bold red]")
562
585
  console.print(f"JSON Error: {e}")
@@ -578,7 +601,7 @@ def main():
578
601
 
579
602
  # Execute the tool
580
603
  try:
581
- success = asyncio.run(call_mcp_tool(args.tool_name, params))
604
+ success = asyncio.run(call_mcp_tool(args.tool_name, params, raw=args.raw))
582
605
  if not success:
583
606
  sys.exit(1)
584
607
  except KeyboardInterrupt:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 2.45
3
+ Version: 2.47
4
4
  Summary: AI-readiness for the agentic web — local-first, Nix-reproducible workflows. The successor to AI SEO software.
5
5
  Author-email: Mike Levin <pipulate@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -77,6 +77,7 @@ tests/test_mck_rep2.py
77
77
  tools/__init__.py
78
78
  tools/advanced_automation_tools.py
79
79
  tools/botify_tools.py
80
+ tools/connector_tools.py
80
81
  tools/conversation_tools.py
81
82
  tools/dom_tools.py
82
83
  tools/keychain_tools.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pipulate"
7
7
  # VERSION NOTE: Synced from pipulate/__init__.py.__version__ via python version_sync.py
8
- version = "2.45"
8
+ version = "2.47"
9
9
  authors = [
10
10
  { name="Mike Levin", email="pipulate@gmail.com" },
11
11
  ]
@@ -4138,7 +4138,6 @@ async def reset_python_env(request):
4138
4138
  await pipulate.stream('📝 **Next Steps Required:**', verbatim=True, role='system')
4139
4139
  await pipulate.stream(' 1. Type `exit` to leave the current nix shell', verbatim=True, role='system')
4140
4140
  await pipulate.stream(' 2. Type `nix develop` to rebuild the environment', verbatim=True, role='system')
4141
- await pipulate.stream(' 3. The fresh environment build will take 2-3 minutes', verbatim=True, role='system')
4142
4141
  await pipulate.stream('', verbatim=True, role='system') # Empty line for spacing
4143
4142
  await pipulate.stream('🚪 Server will exit in 3 seconds...', verbatim=True, role='system')
4144
4143
 
@@ -44,9 +44,41 @@ def get_all_tools():
44
44
  except ImportError as e:
45
45
  print(f"Could not import tool module: {module_name} - {e}")
46
46
 
47
+ denied = denied_tools()
48
+ if denied:
49
+ return {name: func for name, func in AUTO_REGISTERED_TOOLS.items()
50
+ if name not in denied}
47
51
  return AUTO_REGISTERED_TOOLS
52
+
53
+
54
+ def denied_tools():
55
+ """Names withheld from the registry by PIPULATE_TOOL_DENY (comma-separated).
56
+
57
+ THE ESCAPE HATCH, NAMED. execute_shell_command is a registry tool, so any
58
+ experiment that pits the registry against the shell has a registry arm that
59
+ can shell out and become the other arm. Withholding is an ENV VAR on
60
+ purpose: the registry itself never changes, the denial is visible in the
61
+ harness command that set it, and an empty variable is exactly today.
62
+ Applied at get_all_tools()'s return because every cli.py path -- call,
63
+ mcp-discover --all, mcp-discover --tool -- reads that one dict.
64
+ """
65
+ raw = os.environ.get("PIPULATE_TOOL_DENY", "")
66
+ return {name.strip() for name in raw.split(",") if name.strip()}
48
67
  # --- END NEW ---
49
68
 
69
+ # EARMARK (2026-08-30): TWO REGISTRATION LINEAGES, ONE READER. get_all_tools()
70
+ # serves only @auto_tool functions. tools/advanced_automation_tools.py still
71
+ # defines its own execute_shell_command (undecorated -- receipt: rg -n -B2
72
+ # '^async def execute_shell_command' shows a comment and a blank line above
73
+ # it) and hands it to a register_mcp_tool() that feeds a registry THIS package
74
+ # never reads; tools/mcp_tools.py (42 async defs) and tools/botify_tools.py
75
+ # (six core functions on aiohttp and an ai_dictdb credential path, zero
76
+ # decorators) are the same pre-@auto_tool lineage. cli.py cannot reach any of
77
+ # them. Whether server.py's bracket-command path still can is UNWITNESSED.
78
+ # Gut or migrate: each function either gains @auto_tool plus the current
79
+ # credential path (config.get_botify_token, or the subprocess-the-connector
80
+ # pattern in connector_tools.py) or is deleted, and the botify_exports import
81
+ # below leaves with botify_tools.py. Not this ride.
50
82
  __version__ = "1.0.0"
51
83
 
52
84
  # Import shared constants to eliminate duplication
@@ -0,0 +1,98 @@
1
+ """
2
+ connector_tools.py -- the registry face of scripts/connectors/*.py: one tool
3
+ per connector, SAME argument shape, SAME backend, SAME docstring.
4
+
5
+ This file exists for the two-arm experiment (shell verb vs registry tool) and
6
+ for nothing else yet. Arm S runs the connector as a command line; Arm R calls
7
+ the tool below through `cli.py call <name> --json-args '{...}'`. Both arrive at
8
+ the identical subprocess, so the only thing the experiment can measure is the
9
+ invocation grammar and the discovery surface, which is the variable under test.
10
+
11
+ THREE SURFACES, ONE SOURCE. sources_menu.py reads each connector's module
12
+ docstring with ast.get_docstring and prints its first line as the menu row;
13
+ the connector's own --help prints the same docstring (argparse
14
+ description=__doc__); this file reads the same docstring the same way and
15
+ installs it as the tool's __doc__ at import time, which is what
16
+ `cli.py mcp-discover --tool <name>` prints. A description that is better on
17
+ one arm than the other is the confound that would decide the experiment for
18
+ the wrong reason, so no hand-authored description is allowed to exist here.
19
+
20
+ PARAMS MIRROR ARGPARSE DESTS, deliberately, so the JSON a model must write is
21
+ readable off the same --help text the other arm reads: query (the single
22
+ positional; omit it for the identity walk), org, project, max, check.
23
+
24
+ KNOWN SEAM: the AST-derived Tool Roster in prompt_foo.py cannot see a __doc__
25
+ assigned at runtime; it shows the literal placeholder on the function. Named
26
+ here rather than papered over with a copied sentence that would drift.
27
+ """
28
+ import ast
29
+ import asyncio
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ from tools import auto_tool
34
+
35
+ REPO_ROOT = Path(__file__).resolve().parent.parent
36
+ CONNECTORS = REPO_ROOT / "scripts" / "connectors"
37
+ TIMEOUT = 120
38
+ KEY_TRAILER = (
39
+ "\n\nRegistry form: --json-args keys mirror the connector's argument names --"
40
+ " query (the positional; omit for the identity walk), org, project, max, check."
41
+ )
42
+
43
+
44
+ def _connector_doc(name):
45
+ """The connector's module docstring, read the way sources_menu.py reads it."""
46
+ path = CONNECTORS / f"{name}.py"
47
+ try:
48
+ doc = ast.get_docstring(ast.parse(path.read_text(encoding="utf-8")))
49
+ except (OSError, UnicodeDecodeError, SyntaxError) as exc:
50
+ return f"(could not read {path}: {exc.__class__.__name__})"
51
+ return (doc or f"(NO MODULE DOCSTRING in {path})") + KEY_TRAILER
52
+
53
+
54
+ async def _run_connector(name, params):
55
+ """Run scripts/connectors/<name>.py as a subprocess; return its three channels."""
56
+ if not isinstance(params, dict):
57
+ return {"success": False, "error": "params must be a JSON object"}
58
+ argv = [sys.executable, str(CONNECTORS / f"{name}.py")]
59
+ if params.get("query") is not None:
60
+ argv.append(str(params["query"]))
61
+ for flag in ("org", "project"):
62
+ if params.get(flag):
63
+ argv += [f"--{flag}", str(params[flag])]
64
+ if params.get("max") is not None:
65
+ try:
66
+ argv += ["--max", str(int(params["max"]))]
67
+ except (TypeError, ValueError):
68
+ return {"success": False, "error": "max must be an integer"}
69
+ if params.get("check"):
70
+ argv.append("--check")
71
+ try:
72
+ proc = await asyncio.create_subprocess_exec(
73
+ *argv, cwd=str(REPO_ROOT),
74
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
75
+ out, err = await asyncio.wait_for(proc.communicate(), timeout=TIMEOUT)
76
+ except asyncio.TimeoutError:
77
+ proc.kill()
78
+ await proc.wait()
79
+ return {"success": False, "error": f"timed out after {TIMEOUT}s", "argv": argv[1:]}
80
+ except OSError as exc:
81
+ return {"success": False, "error": str(exc), "argv": argv[1:]}
82
+ return {
83
+ "success": proc.returncode == 0,
84
+ "argv": argv[1:],
85
+ "exit_code": proc.returncode,
86
+ "stdout": out.decode("utf-8", errors="replace"),
87
+ "stderr": err.decode("utf-8", errors="replace"),
88
+ }
89
+
90
+
91
+ @auto_tool
92
+ async def botify(params: dict) -> dict:
93
+ """Registry face of scripts/connectors/botify.py; __doc__ is replaced at
94
+ import with that file's own docstring so both arms read one description."""
95
+ return await _run_connector("botify", params)
96
+
97
+
98
+ botify.__doc__ = _connector_doc("botify")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes