celltype-cli 0.1.0__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 (89) hide show
  1. celltype_cli-0.1.0.dist-info/METADATA +267 -0
  2. celltype_cli-0.1.0.dist-info/RECORD +89 -0
  3. celltype_cli-0.1.0.dist-info/WHEEL +4 -0
  4. celltype_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. celltype_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. ct/__init__.py +3 -0
  7. ct/agent/__init__.py +0 -0
  8. ct/agent/case_studies.py +426 -0
  9. ct/agent/config.py +523 -0
  10. ct/agent/doctor.py +544 -0
  11. ct/agent/knowledge.py +523 -0
  12. ct/agent/loop.py +99 -0
  13. ct/agent/mcp_server.py +478 -0
  14. ct/agent/orchestrator.py +733 -0
  15. ct/agent/runner.py +656 -0
  16. ct/agent/sandbox.py +481 -0
  17. ct/agent/session.py +145 -0
  18. ct/agent/system_prompt.py +186 -0
  19. ct/agent/trace_store.py +228 -0
  20. ct/agent/trajectory.py +169 -0
  21. ct/agent/types.py +182 -0
  22. ct/agent/workflows.py +462 -0
  23. ct/api/__init__.py +1 -0
  24. ct/api/app.py +211 -0
  25. ct/api/config.py +120 -0
  26. ct/api/engine.py +124 -0
  27. ct/cli.py +1448 -0
  28. ct/data/__init__.py +0 -0
  29. ct/data/compute_providers.json +59 -0
  30. ct/data/cro_database.json +395 -0
  31. ct/data/downloader.py +238 -0
  32. ct/data/loaders.py +252 -0
  33. ct/kb/__init__.py +5 -0
  34. ct/kb/benchmarks.py +147 -0
  35. ct/kb/governance.py +106 -0
  36. ct/kb/ingest.py +415 -0
  37. ct/kb/reasoning.py +129 -0
  38. ct/kb/schema_monitor.py +162 -0
  39. ct/kb/substrate.py +387 -0
  40. ct/models/__init__.py +0 -0
  41. ct/models/llm.py +370 -0
  42. ct/tools/__init__.py +195 -0
  43. ct/tools/_compound_resolver.py +297 -0
  44. ct/tools/biomarker.py +368 -0
  45. ct/tools/cellxgene.py +282 -0
  46. ct/tools/chemistry.py +1371 -0
  47. ct/tools/claude.py +390 -0
  48. ct/tools/clinical.py +1153 -0
  49. ct/tools/clue.py +249 -0
  50. ct/tools/code.py +1069 -0
  51. ct/tools/combination.py +397 -0
  52. ct/tools/compute.py +402 -0
  53. ct/tools/cro.py +413 -0
  54. ct/tools/data_api.py +2114 -0
  55. ct/tools/design.py +295 -0
  56. ct/tools/dna.py +575 -0
  57. ct/tools/experiment.py +604 -0
  58. ct/tools/expression.py +655 -0
  59. ct/tools/files.py +957 -0
  60. ct/tools/genomics.py +1387 -0
  61. ct/tools/http_client.py +146 -0
  62. ct/tools/imaging.py +319 -0
  63. ct/tools/intel.py +223 -0
  64. ct/tools/literature.py +743 -0
  65. ct/tools/network.py +422 -0
  66. ct/tools/notification.py +111 -0
  67. ct/tools/omics.py +3330 -0
  68. ct/tools/ops.py +1230 -0
  69. ct/tools/parity.py +649 -0
  70. ct/tools/pk.py +245 -0
  71. ct/tools/protein.py +678 -0
  72. ct/tools/regulatory.py +643 -0
  73. ct/tools/remote_data.py +179 -0
  74. ct/tools/report.py +181 -0
  75. ct/tools/repurposing.py +376 -0
  76. ct/tools/safety.py +1280 -0
  77. ct/tools/shell.py +178 -0
  78. ct/tools/singlecell.py +533 -0
  79. ct/tools/statistics.py +552 -0
  80. ct/tools/structure.py +882 -0
  81. ct/tools/target.py +901 -0
  82. ct/tools/translational.py +123 -0
  83. ct/tools/viability.py +218 -0
  84. ct/ui/__init__.py +0 -0
  85. ct/ui/markdown.py +31 -0
  86. ct/ui/status.py +258 -0
  87. ct/ui/suggestions.py +567 -0
  88. ct/ui/terminal.py +1456 -0
  89. ct/ui/traces.py +112 -0
ct/tools/shell.py ADDED
@@ -0,0 +1,178 @@
1
+ """
2
+ Shell execution tool for ct.
3
+
4
+ Run shell commands from within ct research workflows with safety restrictions.
5
+ """
6
+
7
+ import re
8
+ import shlex
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from ct.tools import registry
13
+
14
+ # Commands/patterns that are never allowed
15
+ _BLOCKED_PATTERNS = (
16
+ "sudo ",
17
+ "rm -rf /",
18
+ "rm -rf /*",
19
+ "chmod 777",
20
+ ":(){ :|:& };:", # fork bomb
21
+ "mkfs.",
22
+ "dd if=",
23
+ "> /dev/sd",
24
+ "shutdown",
25
+ "reboot",
26
+ "init 0",
27
+ "init 6",
28
+ )
29
+
30
+ _BLOCKED_BINARIES = {
31
+ "sudo", "su", "rm", "rmdir", "mkfs", "dd", "shutdown", "reboot", "poweroff",
32
+ "halt", "init", "chown", "chgrp", "chmod", "useradd", "userdel", "groupadd",
33
+ "groupdel", "passwd", "mount", "umount",
34
+ }
35
+
36
+ _UNSAFE_SHELL_SYNTAX = re.compile(r"(\|\|)|(&&)|[;<>`]|[$][(]")
37
+
38
+ # Safe commands that may appear on the right side of a pipe
39
+ _SAFE_PIPE_RHS = {
40
+ "head", "tail", "grep", "wc", "sort", "uniq", "cut", "awk", "sed",
41
+ "cat", "less", "more", "tr", "tee", "xargs",
42
+ }
43
+
44
+
45
+ def _is_blocked(command: str) -> str | None:
46
+ """Return a reason string if the command is blocked, else None."""
47
+ cmd = (command or "").strip()
48
+ if not cmd:
49
+ return "Empty command"
50
+
51
+ # Check for unsafe syntax (excluding single pipe |)
52
+ if _UNSAFE_SHELL_SYNTAX.search(cmd):
53
+ return "Shell operators/redirection are not allowed; run a single command only"
54
+
55
+ # Allow single-pipe commands where the RHS is a safe utility
56
+ if "|" in cmd:
57
+ parts = cmd.split("|")
58
+ if len(parts) > 3:
59
+ return "Too many pipes; simplify the command"
60
+ for part in parts[1:]: # Check RHS commands
61
+ part_stripped = part.strip()
62
+ if not part_stripped:
63
+ return "Empty pipe segment"
64
+ try:
65
+ first_token = shlex.split(part_stripped, posix=True)[0]
66
+ rhs_name = Path(first_token).name.lower()
67
+ except (ValueError, IndexError):
68
+ return f"Invalid pipe segment: {part_stripped[:50]}"
69
+ if rhs_name not in _SAFE_PIPE_RHS:
70
+ return f"Pipe to '{rhs_name}' not allowed; only safe utilities permitted after pipe"
71
+
72
+ cmd_lower = cmd.lower()
73
+ for pattern in _BLOCKED_PATTERNS:
74
+ if pattern.lower() in cmd_lower:
75
+ return f"Blocked command pattern: {pattern}"
76
+
77
+ try:
78
+ tokens = shlex.split(cmd, posix=True)
79
+ except ValueError as e:
80
+ return f"Invalid command syntax: {e}"
81
+
82
+ if not tokens:
83
+ return "Empty command"
84
+
85
+ command_name = Path(tokens[0]).name.lower()
86
+ if command_name in _BLOCKED_BINARIES:
87
+ return f"Blocked command: {command_name}"
88
+
89
+ if command_name in {"python", "python3", "bash", "sh", "zsh", "node", "perl", "ruby"}:
90
+ if any(tok in {"-c", "-e"} for tok in tokens[1:]):
91
+ return f"Blocked inline script execution for {command_name}"
92
+
93
+ return None
94
+
95
+
96
+ @registry.register(
97
+ name="shell.run",
98
+ description="Run a shell command in the current working directory",
99
+ category="shell",
100
+ parameters={
101
+ "command": "Shell command to execute",
102
+ "timeout": "Timeout in seconds (default 30, max 300)",
103
+ },
104
+ usage_guide=(
105
+ "Use to run a single shell command: scripts, data processing, git, pip, etc. "
106
+ "Commands run in the current working directory. Dangerous commands and shell "
107
+ "operators (pipes/redirection/chaining) are blocked for safety."
108
+ ),
109
+ )
110
+ def shell_run(command: str, timeout: int = 30, **kwargs) -> dict:
111
+ """Run a shell command and return stdout/stderr."""
112
+ # Safety check
113
+ blocked = _is_blocked(command)
114
+ if blocked:
115
+ return {"summary": f"Command blocked: {blocked}", "error": "blocked_command"}
116
+
117
+ # Cap timeout
118
+ timeout = min(max(timeout, 1), 300)
119
+
120
+ # Use shell=True for pipe commands, shell=False otherwise
121
+ use_shell = "|" in command
122
+ if use_shell:
123
+ run_args = command
124
+ else:
125
+ try:
126
+ run_args = shlex.split(command, posix=True)
127
+ except ValueError as e:
128
+ return {
129
+ "summary": f"Invalid command syntax: {e}",
130
+ "error": "invalid_command",
131
+ "exit_code": -1,
132
+ }
133
+
134
+ try:
135
+ result = subprocess.run(
136
+ run_args,
137
+ shell=use_shell,
138
+ cwd=str(Path.cwd()),
139
+ capture_output=True,
140
+ text=True,
141
+ timeout=timeout,
142
+ )
143
+ except subprocess.TimeoutExpired:
144
+ return {
145
+ "summary": f"Command timed out after {timeout}s: {command[:80]}",
146
+ "error": "timeout",
147
+ "exit_code": -1,
148
+ }
149
+ except Exception as e:
150
+ return {
151
+ "summary": f"Command failed: {e}",
152
+ "error": str(e),
153
+ "exit_code": -1,
154
+ }
155
+
156
+ stdout = result.stdout
157
+ stderr = result.stderr
158
+
159
+ # Truncate large output
160
+ if len(stdout) > 10000:
161
+ stdout = stdout[:10000] + f"\n... [truncated, total {len(result.stdout)} chars]"
162
+ if len(stderr) > 5000:
163
+ stderr = stderr[:5000] + f"\n... [truncated, total {len(result.stderr)} chars]"
164
+
165
+ if result.returncode == 0:
166
+ output_preview = stdout.strip()[:200] if stdout.strip() else "(no output)"
167
+ summary = f"Command succeeded (exit 0): {output_preview}"
168
+ else:
169
+ err_preview = stderr.strip()[:200] if stderr.strip() else stdout.strip()[:200]
170
+ summary = f"Command failed (exit {result.returncode}): {err_preview}"
171
+
172
+ return {
173
+ "summary": summary,
174
+ "exit_code": result.returncode,
175
+ "stdout": stdout,
176
+ "stderr": stderr,
177
+ "command": command,
178
+ }