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.
- package/.dockerignore +14 -0
- package/.env.example +24 -0
- package/Dockerfile +62 -0
- package/cli/__init__.py +0 -0
- package/cli/branding.py +105 -0
- package/cli/crowe_logic.py +268 -0
- package/cli/icon.png +0 -0
- package/config/__init__.py +0 -0
- package/config/agent_config.py +54 -0
- package/npm/bin.js +34 -0
- package/package.json +32 -0
- package/pyproject.toml +70 -0
- package/requirements.txt +11 -0
- package/scripts/__init__.py +0 -0
- package/scripts/create_agent.py +163 -0
- package/scripts/fine_tune.py +271 -0
- package/scripts/npm-publish.sh +15 -0
- package/scripts/orchestrator.applescript +162 -0
- package/scripts/test_agent.py +200 -0
- package/setup.py +24 -0
- package/tools/__init__.py +57 -0
- package/tools/applescript.py +58 -0
- package/tools/browser.py +79 -0
- package/tools/filesystem.py +115 -0
- package/tools/git_ops.py +107 -0
- package/tools/playwright_browser.py +101 -0
- package/tools/quantum.py +89 -0
- package/tools/search.py +108 -0
- package/tools/shell.py +47 -0
- package/tools/talon_music.py +112 -0
|
@@ -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)})
|
package/tools/quantum.py
ADDED
|
@@ -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)})
|
package/tools/search.py
ADDED
|
@@ -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)})
|