crowe-logic 0.1.0

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.
@@ -0,0 +1,101 @@
1
+ """
2
+ Playwright browser automation tool — full browser control via MCP.
3
+ Wraps the @playwright/mcp server as a locally callable tool.
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+
9
+
10
+ def browser_navigate(url: str) -> str:
11
+ """
12
+ Navigate the browser to a URL and return the page accessibility snapshot.
13
+ Opens a browser if one isn't already running.
14
+
15
+ :param url: The URL to navigate to.
16
+ :return: JSON with page title and accessibility tree snapshot.
17
+ :rtype: str
18
+ """
19
+ return _run_playwright_action("browser_navigate", {"url": url})
20
+
21
+
22
+ def browser_click(element: str, ref: str = "") -> str:
23
+ """
24
+ Click an element on the current page. Use the element description
25
+ or the ref ID from a previous snapshot.
26
+
27
+ :param element: Description of the element to click (e.g. "Submit button", "Login link").
28
+ :param ref: Optional element ref from accessibility snapshot.
29
+ :return: JSON result of the click action.
30
+ :rtype: str
31
+ """
32
+ args = {"element": element}
33
+ if ref:
34
+ args["ref"] = ref
35
+ return _run_playwright_action("browser_click", args)
36
+
37
+
38
+ def browser_type_text(element: str, text: str, submit: bool = False) -> str:
39
+ """
40
+ Type text into an input field on the page.
41
+
42
+ :param element: Description of the input field (e.g. "Search box", "Email field").
43
+ :param text: The text to type.
44
+ :param submit: Whether to press Enter after typing (default False).
45
+ :return: JSON result of the type action.
46
+ :rtype: str
47
+ """
48
+ args = {"element": element, "text": text, "submit": submit}
49
+ return _run_playwright_action("browser_type", args)
50
+
51
+
52
+ def browser_snapshot() -> str:
53
+ """
54
+ Get the current page's accessibility tree snapshot. This shows all
55
+ interactive elements, text content, and their ref IDs for clicking.
56
+
57
+ :return: JSON with the full page accessibility snapshot.
58
+ :rtype: str
59
+ """
60
+ return _run_playwright_action("browser_snapshot", {})
61
+
62
+
63
+ def browser_screenshot(filename: str = "/tmp/screenshot.png") -> str:
64
+ """
65
+ Take a screenshot of the current browser page.
66
+
67
+ :param filename: Path to save the screenshot (default /tmp/screenshot.png).
68
+ :return: JSON with the screenshot file path.
69
+ :rtype: str
70
+ """
71
+ return _run_playwright_action("browser_take_screenshot", {"raw": True})
72
+
73
+
74
+ def _run_playwright_action(tool_name: str, arguments: dict) -> str:
75
+ """Bridge to the Playwright MCP server via npx."""
76
+ try:
77
+ # Use the MCP server's stdio interface via a subprocess
78
+ mcp_input = json.dumps({
79
+ "jsonrpc": "2.0",
80
+ "id": 1,
81
+ "method": "tools/call",
82
+ "params": {"name": tool_name, "arguments": arguments}
83
+ })
84
+
85
+ result = subprocess.run(
86
+ ["npx", "-y", "@playwright/mcp@latest", "--headless"],
87
+ input=mcp_input,
88
+ capture_output=True,
89
+ text=True,
90
+ timeout=30,
91
+ cwd="/Users/crowelogic/Projects/crowe-logic-foundry",
92
+ )
93
+
94
+ if result.stdout:
95
+ return result.stdout[:50000]
96
+ return json.dumps({"note": "Action sent to Playwright", "tool": tool_name, "args": arguments})
97
+
98
+ except subprocess.TimeoutExpired:
99
+ return json.dumps({"error": f"Playwright action timed out: {tool_name}"})
100
+ except Exception as e:
101
+ return json.dumps({"error": str(e)})
@@ -0,0 +1,89 @@
1
+ """
2
+ Quantum computing tool — execute circuits via Qiskit, Cirq, PennyLane, and Synapse.
3
+ """
4
+
5
+ import json
6
+
7
+
8
+ def run_quantum_circuit(code: str, backend: str = "qiskit", shots: int = 1024) -> str:
9
+ """
10
+ Execute a quantum circuit using the specified backend.
11
+ Supports Qiskit (IBM Quantum), Cirq (Google), PennyLane, and Synapse-Lang.
12
+
13
+ :param code: Python code defining the quantum circuit. Must assign result to a variable called 'result'.
14
+ :param backend: Quantum framework (qiskit, cirq, pennylane, synapse).
15
+ :param shots: Number of measurement shots (default 1024).
16
+ :return: JSON with circuit execution results (counts, probabilities, statevector).
17
+ :rtype: str
18
+ """
19
+ try:
20
+ # Create a safe execution namespace with quantum imports
21
+ namespace = {"__builtins__": __builtins__, "shots": shots}
22
+
23
+ if backend == "qiskit":
24
+ import qiskit
25
+ from qiskit_aer import AerSimulator
26
+ namespace["QuantumCircuit"] = qiskit.QuantumCircuit
27
+ namespace["AerSimulator"] = AerSimulator
28
+ elif backend == "cirq":
29
+ import cirq
30
+ import numpy as np
31
+ namespace["cirq"] = cirq
32
+ namespace["np"] = np
33
+ elif backend == "pennylane":
34
+ import pennylane as qml
35
+ import numpy as np
36
+ namespace["qml"] = qml
37
+ namespace["np"] = np
38
+ elif backend == "synapse":
39
+ import synapse_lang
40
+ namespace["synapse_lang"] = synapse_lang
41
+
42
+ exec(code, namespace)
43
+
44
+ result = namespace.get("result", "No 'result' variable found. Assign your output to 'result'.")
45
+
46
+ # Serialize the result
47
+ if hasattr(result, "to_dict"):
48
+ return json.dumps({"backend": backend, "result": result.to_dict()})
49
+ else:
50
+ return json.dumps({"backend": backend, "result": str(result)})
51
+
52
+ except Exception as e:
53
+ return json.dumps({"error": str(e), "backend": backend})
54
+
55
+
56
+ def synapse_evaluate(expression: str) -> str:
57
+ """
58
+ Evaluate a Synapse-Lang expression. Synapse is a quantum-classical
59
+ hybrid programming language created by Michael Crowe.
60
+
61
+ :param expression: A Synapse-Lang expression or program.
62
+ :return: JSON with the evaluation result.
63
+ :rtype: str
64
+ """
65
+ try:
66
+ from synapse_lang import SynapseLang
67
+ sl = SynapseLang()
68
+ result = sl.evaluate(expression)
69
+ return json.dumps({"expression": expression, "result": str(result)})
70
+ except Exception as e:
71
+ return json.dumps({"error": str(e), "expression": expression})
72
+
73
+
74
+ def qubit_flow_execute(program: str) -> str:
75
+ """
76
+ Execute a Qubit-Flow program. Qubit-Flow is a quantum circuit design
77
+ language that's part of the Quantum Trinity (with Synapse-Lang).
78
+
79
+ :param program: A Qubit-Flow program string.
80
+ :return: JSON with execution results.
81
+ :rtype: str
82
+ """
83
+ try:
84
+ from qubit_flow_lang import QubitFlowInterpreter
85
+ interpreter = QubitFlowInterpreter()
86
+ result = interpreter.run(program)
87
+ return json.dumps({"program": program[:500], "result": str(result)})
88
+ except Exception as e:
89
+ return json.dumps({"error": str(e)})
@@ -0,0 +1,108 @@
1
+ """
2
+ Search tools — web search and local file content search (grep).
3
+ """
4
+
5
+ import json
6
+ import subprocess
7
+ import os
8
+
9
+
10
+ def web_search(query: str, num_results: int = 5) -> str:
11
+ """
12
+ Search the web using the system's available search capabilities.
13
+ Returns a list of search results with titles, URLs, and snippets.
14
+
15
+ :param query: The search query string.
16
+ :param num_results: Number of results to return (default 5, max 10).
17
+ :return: JSON list of search results.
18
+ :rtype: str
19
+ """
20
+ import httpx
21
+
22
+ num_results = min(num_results, 10)
23
+
24
+ # Use DuckDuckGo Lite as a free, no-API-key search fallback
25
+ try:
26
+ response = httpx.get(
27
+ "https://lite.duckduckgo.com/lite/",
28
+ params={"q": query},
29
+ headers={"User-Agent": "CroweLogic/0.1"},
30
+ timeout=15,
31
+ follow_redirects=True,
32
+ )
33
+ # Parse the lite HTML for result links
34
+ from bs4 import BeautifulSoup
35
+ soup = BeautifulSoup(response.text, "html.parser")
36
+
37
+ results = []
38
+ for link in soup.select("a.result-link, td a[href^='http']"):
39
+ href = link.get("href", "")
40
+ text = link.get_text(strip=True)
41
+ if href.startswith("http") and text and len(results) < num_results:
42
+ results.append({"title": text, "url": href})
43
+
44
+ if results:
45
+ return json.dumps({"query": query, "results": results})
46
+
47
+ return json.dumps({"query": query, "results": [], "note": "No results found. Try rephrasing."})
48
+ except Exception as e:
49
+ return json.dumps({"error": f"Search failed: {str(e)}"})
50
+
51
+
52
+ def grep_search(pattern: str, path: str = ".", file_glob: str = "", max_results: int = 50) -> str:
53
+ """
54
+ Search file contents using ripgrep (rg) or grep. Supports regex patterns.
55
+
56
+ :param pattern: Regex pattern to search for.
57
+ :param path: Directory or file to search in (default: current directory).
58
+ :param file_glob: Optional glob to filter files (e.g. "*.py", "*.ts").
59
+ :param max_results: Maximum number of matching lines to return (default 50).
60
+ :return: JSON with matching lines, file paths, and line numbers.
61
+ :rtype: str
62
+ """
63
+ search_path = os.path.expanduser(path)
64
+ max_results = min(max_results, 200)
65
+
66
+ # Prefer ripgrep, fall back to grep
67
+ rg_path = subprocess.run(["which", "rg"], capture_output=True, text=True).stdout.strip()
68
+
69
+ try:
70
+ if rg_path:
71
+ cmd = [rg_path, "--json", "-m", str(max_results), "--no-heading"]
72
+ if file_glob:
73
+ cmd.extend(["--glob", file_glob])
74
+ cmd.extend([pattern, search_path])
75
+ else:
76
+ cmd = ["grep", "-rn", "--include", file_glob or "*", pattern, search_path]
77
+
78
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
79
+
80
+ if rg_path:
81
+ # Parse ripgrep JSON output
82
+ matches = []
83
+ for line in result.stdout.splitlines():
84
+ try:
85
+ entry = json.loads(line)
86
+ if entry.get("type") == "match":
87
+ data = entry["data"]
88
+ matches.append({
89
+ "file": data["path"]["text"],
90
+ "line_number": data["line_number"],
91
+ "text": data["lines"]["text"].rstrip(),
92
+ })
93
+ except json.JSONDecodeError:
94
+ continue
95
+ return json.dumps({"pattern": pattern, "count": len(matches), "matches": matches})
96
+ else:
97
+ # Parse grep output
98
+ matches = []
99
+ for line in result.stdout.splitlines()[:max_results]:
100
+ parts = line.split(":", 2)
101
+ if len(parts) >= 3:
102
+ matches.append({"file": parts[0], "line_number": int(parts[1]), "text": parts[2].rstrip()})
103
+ return json.dumps({"pattern": pattern, "count": len(matches), "matches": matches})
104
+
105
+ except subprocess.TimeoutExpired:
106
+ return json.dumps({"error": "Search timed out after 30s"})
107
+ except Exception as e:
108
+ return json.dumps({"error": str(e)})
package/tools/shell.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ Shell tool — execute commands locally.
3
+ """
4
+
5
+ import json
6
+ import subprocess
7
+
8
+
9
+ def execute_shell(command: str, working_directory: str = "", timeout_seconds: int = 120) -> str:
10
+ """
11
+ Execute a shell command and return stdout, stderr, and exit code.
12
+ Commands run in a bash shell with a configurable timeout.
13
+
14
+ :param command: The shell command to execute.
15
+ :param working_directory: Directory to run the command in (default: home dir).
16
+ :param timeout_seconds: Max execution time in seconds (default 120, max 600).
17
+ :return: JSON with stdout, stderr, and return_code.
18
+ :rtype: str
19
+ """
20
+ import os
21
+
22
+ cwd = working_directory or os.path.expanduser("~")
23
+ timeout_seconds = min(timeout_seconds, 600)
24
+
25
+ try:
26
+ result = subprocess.run(
27
+ command,
28
+ shell=True,
29
+ capture_output=True,
30
+ text=True,
31
+ cwd=cwd,
32
+ timeout=timeout_seconds,
33
+ env={**os.environ, "TERM": "dumb"},
34
+ )
35
+ stdout = result.stdout
36
+ if len(stdout) > 50000:
37
+ stdout = stdout[:50000] + "\n... (output truncated at 50KB)"
38
+
39
+ return json.dumps({
40
+ "stdout": stdout,
41
+ "stderr": result.stderr[:10000] if result.stderr else "",
42
+ "return_code": result.returncode,
43
+ })
44
+ except subprocess.TimeoutExpired:
45
+ return json.dumps({"error": f"Command timed out after {timeout_seconds}s", "return_code": -1})
46
+ except Exception as e:
47
+ return json.dumps({"error": str(e), "return_code": -1})
@@ -0,0 +1,112 @@
1
+ """
2
+ Talon Music Engine tool — quantum-powered composition via @talon/* packages.
3
+ Interfaces with the Talon CLI and core libraries at ~/Projects/talon/.
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+ import os
9
+
10
+ TALON_PATH = "/Users/crowelogic/Projects/talon"
11
+
12
+
13
+ def talon_compose(style: str = "ambient", duration_bars: int = 16, quantum_mode: str = "superposition") -> str:
14
+ """
15
+ Generate a musical composition using the Talon engine.
16
+ Leverages quantum-enhanced algorithms for creative variation.
17
+
18
+ :param style: Musical style (ambient, cinematic, electronic, jazz, experimental).
19
+ :param duration_bars: Length in bars (default 16).
20
+ :param quantum_mode: Quantum generation mode (superposition, entanglement, interference).
21
+ :return: JSON with composition data (MIDI events, structure, quantum metrics).
22
+ :rtype: str
23
+ """
24
+ return _run_talon_cli(["compose", "--style", style, "--bars", str(duration_bars), "--quantum", quantum_mode])
25
+
26
+
27
+ def talon_import_midi(midi_path: str) -> str:
28
+ """
29
+ Import a MIDI file into Talon for analysis and transformation.
30
+
31
+ :param midi_path: Absolute path to the MIDI file.
32
+ :return: JSON with imported track data (notes, tempo, structure).
33
+ :rtype: str
34
+ """
35
+ return _run_talon_cli(["import", midi_path])
36
+
37
+
38
+ def talon_analyze(input_source: str) -> str:
39
+ """
40
+ Analyze a MIDI file or Talon composition for musical properties.
41
+
42
+ :param input_source: Path to MIDI file or Talon composition ID.
43
+ :return: JSON with analysis (key, tempo, harmony, rhythm, complexity).
44
+ :rtype: str
45
+ """
46
+ return _run_talon_cli(["analyze", input_source])
47
+
48
+
49
+ def talon_transform(input_source: str, transformation: str) -> str:
50
+ """
51
+ Apply a quantum transformation to a composition or MIDI file.
52
+
53
+ :param input_source: Path to MIDI file or Talon composition ID.
54
+ :param transformation: Transformation to apply (transpose, invert, retrograde, quantum-evolve, fractal-expand).
55
+ :return: JSON with transformed composition data.
56
+ :rtype: str
57
+ """
58
+ return _run_talon_cli(["transform", input_source, "--type", transformation])
59
+
60
+
61
+ def talon_export(composition_id: str, format: str = "midi", output_path: str = "") -> str:
62
+ """
63
+ Export a Talon composition to a file.
64
+
65
+ :param composition_id: The composition ID to export.
66
+ :param format: Export format (midi, wav, json, ableton-als).
67
+ :param output_path: Optional output path (default: ~/Desktop/).
68
+ :return: JSON with export result and file path.
69
+ :rtype: str
70
+ """
71
+ cmd = ["export", composition_id, "--format", format]
72
+ if output_path:
73
+ cmd.extend(["--output", output_path])
74
+ return _run_talon_cli(cmd)
75
+
76
+
77
+ def _run_talon_cli(args: list) -> str:
78
+ """Run a Talon CLI command."""
79
+ if not os.path.isdir(TALON_PATH):
80
+ return json.dumps({"error": f"Talon project not found at {TALON_PATH}"})
81
+
82
+ try:
83
+ # Try the CLI first, fall back to npx/node
84
+ result = subprocess.run(
85
+ ["npx", "talon"] + args,
86
+ capture_output=True, text=True, timeout=60,
87
+ cwd=TALON_PATH,
88
+ env={**os.environ, "NODE_PATH": os.path.join(TALON_PATH, "node_modules")},
89
+ )
90
+
91
+ if result.returncode != 0 and "not found" in result.stderr.lower():
92
+ # Fall back to direct node execution
93
+ cli_path = os.path.join(TALON_PATH, "packages", "cli", "src", "index.ts")
94
+ result = subprocess.run(
95
+ ["npx", "tsx", cli_path] + args,
96
+ capture_output=True, text=True, timeout=60,
97
+ cwd=TALON_PATH,
98
+ )
99
+
100
+ output = result.stdout.strip()
101
+ if len(output) > 50000:
102
+ output = output[:50000] + "\n... (truncated)"
103
+
104
+ return json.dumps({
105
+ "output": output,
106
+ "stderr": result.stderr.strip() if result.stderr else "",
107
+ "return_code": result.returncode,
108
+ })
109
+ except subprocess.TimeoutExpired:
110
+ return json.dumps({"error": "Talon command timed out after 60s"})
111
+ except Exception as e:
112
+ return json.dumps({"error": str(e)})