sql-harness 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.
- sql_harness/SKILL.md +61 -0
- sql_harness/__init__.py +27 -0
- sql_harness/agent_loader.py +55 -0
- sql_harness/cli.py +869 -0
- sql_harness/config.py +241 -0
- sql_harness/drivers/__init__.py +59 -0
- sql_harness/drivers/mysql.py +43 -0
- sql_harness/drivers/postgres.py +55 -0
- sql_harness/drivers/redis.py +33 -0
- sql_harness/drivers/sqlite.py +39 -0
- sql_harness/drivers/ssh.py +293 -0
- sql_harness/helpers.py +789 -0
- sql_harness/manager.py +122 -0
- sql_harness/paths.py +112 -0
- sql_harness/run.py +85 -0
- sql_harness-0.1.0.dist-info/METADATA +137 -0
- sql_harness-0.1.0.dist-info/RECORD +19 -0
- sql_harness-0.1.0.dist-info/WHEEL +4 -0
- sql_harness-0.1.0.dist-info/entry_points.txt +2 -0
sql_harness/manager.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""SqlHarness — single-process manager holding open workspaces.
|
|
2
|
+
|
|
3
|
+
A workspace = a named connection + its SQLAlchemy Engine. Engines are opened
|
|
4
|
+
lazily and kept alive until `close_workspace()` or process exit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import Engine
|
|
12
|
+
|
|
13
|
+
from .config import ConnectionsConfig
|
|
14
|
+
from .drivers import Driver, get_driver
|
|
15
|
+
from .paths import workspace_dir
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Workspace:
|
|
19
|
+
"""One connection = one workspace."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: "WorkspaceConfig", engine: Engine, driver):
|
|
22
|
+
self.config = config
|
|
23
|
+
self.engine = engine
|
|
24
|
+
self.driver = driver
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return f"Workspace(name={self.config.connection.name!r}, driver={self.driver.name})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorkspaceConfig:
|
|
31
|
+
"""Lightweight wrapper used at runtime; created from ConnectionConfig."""
|
|
32
|
+
|
|
33
|
+
__slots__ = ("connection",)
|
|
34
|
+
|
|
35
|
+
def __init__(self, connection):
|
|
36
|
+
self.connection = connection
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SqlHarness:
|
|
40
|
+
"""Owns the open workspace registry."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config: ConnectionsConfig):
|
|
43
|
+
self.config = config
|
|
44
|
+
self._workspaces: dict[str, Workspace] = {}
|
|
45
|
+
|
|
46
|
+
# --- workspace lifecycle -----------------------------------------------
|
|
47
|
+
|
|
48
|
+
def workspace(self, name: str) -> Workspace:
|
|
49
|
+
"""Open (or return cached) workspace for `name`."""
|
|
50
|
+
if name in self._workspaces:
|
|
51
|
+
return self._workspaces[name]
|
|
52
|
+
conn = self.config.get(name)
|
|
53
|
+
driver = get_driver(conn.driver)
|
|
54
|
+
engine = driver.make_engine(conn.url, conn.pool)
|
|
55
|
+
ws = Workspace(WorkspaceConfig(conn), engine, driver)
|
|
56
|
+
self._workspaces[name] = ws
|
|
57
|
+
return ws
|
|
58
|
+
|
|
59
|
+
def close_workspace(self, name: str) -> None:
|
|
60
|
+
ws = self._workspaces.pop(name, None)
|
|
61
|
+
if ws is not None:
|
|
62
|
+
# SQLAlchemy engines use .dispose(); SSH handles use .close().
|
|
63
|
+
closer = getattr(ws.engine, "close", None)
|
|
64
|
+
if callable(closer):
|
|
65
|
+
closer()
|
|
66
|
+
else:
|
|
67
|
+
ws.engine.dispose()
|
|
68
|
+
|
|
69
|
+
def list_workspaces(self) -> list[str]:
|
|
70
|
+
return sorted(self._workspaces.keys())
|
|
71
|
+
|
|
72
|
+
def has_workspace(self, name: str) -> bool:
|
|
73
|
+
return name in self._workspaces
|
|
74
|
+
|
|
75
|
+
def get_workspace(self, name: str) -> Workspace | None:
|
|
76
|
+
return self._workspaces.get(name)
|
|
77
|
+
|
|
78
|
+
# --- default workspace (from connections.toml) -----------------------
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def default_workspace_name(self) -> str:
|
|
82
|
+
return self.config.default_workspace
|
|
83
|
+
|
|
84
|
+
def default_workspace(self) -> Workspace | None:
|
|
85
|
+
name = self.config.default_workspace
|
|
86
|
+
if not name:
|
|
87
|
+
return None
|
|
88
|
+
if name not in self.config.names():
|
|
89
|
+
return None
|
|
90
|
+
return self.workspace(name)
|
|
91
|
+
|
|
92
|
+
# --- skills ------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def apply_skill(self, name: str) -> str:
|
|
95
|
+
"""Read an agent-workspace skill markdown file."""
|
|
96
|
+
path = self._skill_path(name)
|
|
97
|
+
if not path.is_file():
|
|
98
|
+
raise FileNotFoundError(
|
|
99
|
+
f"no skill named {name!r} in {self._skills_dir()}"
|
|
100
|
+
)
|
|
101
|
+
return path.read_text(encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
def list_skills(self) -> list[str]:
|
|
104
|
+
d = self._skills_dir()
|
|
105
|
+
if not d.is_dir():
|
|
106
|
+
return []
|
|
107
|
+
return sorted(p.stem for p in d.glob("*.md") if p.is_file())
|
|
108
|
+
|
|
109
|
+
def _skill_path(self, name: str) -> Path:
|
|
110
|
+
# Defensive: prevent path traversal.
|
|
111
|
+
if "/" in name or "\\" in name or name.startswith("."):
|
|
112
|
+
raise ValueError(f"invalid skill name: {name!r}")
|
|
113
|
+
return self._skills_dir() / f"{name}.md"
|
|
114
|
+
|
|
115
|
+
def _skills_dir(self) -> Path:
|
|
116
|
+
return workspace_dir() / "skills"
|
|
117
|
+
|
|
118
|
+
# --- teardown ----------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def close_all(self) -> None:
|
|
121
|
+
for name in list(self._workspaces.keys()):
|
|
122
|
+
self.close_workspace(name)
|
sql_harness/paths.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Path resolution for sql-harness state directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def home_dir() -> Path:
|
|
13
|
+
"""Return the sql-harness state root.
|
|
14
|
+
|
|
15
|
+
Resolved in order:
|
|
16
|
+
1. $BH_SQL_HOME (matches browser-harness env-var style)
|
|
17
|
+
2. $XDG_CONFIG_HOME/sql-harness
|
|
18
|
+
3. ~/.config/sql-harness (POSIX / fallback)
|
|
19
|
+
"""
|
|
20
|
+
override = os.environ.get("BH_SQL_HOME")
|
|
21
|
+
if override:
|
|
22
|
+
return Path(override).expanduser()
|
|
23
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
24
|
+
if xdg:
|
|
25
|
+
return Path(xdg).expanduser() / "sql-harness"
|
|
26
|
+
return Path.home() / ".config" / "sql-harness"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def config_dir() -> Path:
|
|
30
|
+
"""Directory holding connections.toml and other static config."""
|
|
31
|
+
override = os.environ.get("BH_SQL_CONFIG_DIR")
|
|
32
|
+
return Path(override).expanduser() if override else home_dir()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def config_file() -> Path:
|
|
36
|
+
"""Path to the plaintext connections.toml."""
|
|
37
|
+
override = os.environ.get("BH_SQL_CONFIG_FILE")
|
|
38
|
+
return Path(override).expanduser() if override else config_dir() / "connections.toml"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def workspace_dir() -> Path:
|
|
42
|
+
"""Agent-editable workspace (mirrors browser-harness agent-workspace/).
|
|
43
|
+
|
|
44
|
+
Layout:
|
|
45
|
+
- agent_helpers.py shared base helpers (auto-imported every run)
|
|
46
|
+
- zones/<connection>/ per-DSN isolated zone (mirrors domain-skills/<host>/)
|
|
47
|
+
- helpers.py per-connection helpers (merged over the shared base)
|
|
48
|
+
- scripts/<name>.py saved heredocs for this connection
|
|
49
|
+
- skills/<name>.md per-connection skill knowledge
|
|
50
|
+
- scripts/ (legacy global scripts, pre-zone)
|
|
51
|
+
- skills/ (legacy global skills, pre-zone)
|
|
52
|
+
"""
|
|
53
|
+
override = os.environ.get("BH_SQL_AGENT_WORKSPACE")
|
|
54
|
+
return Path(override).expanduser() if override else home_dir() / "agent-workspace"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def scripts_dir() -> Path:
|
|
58
|
+
"""Legacy global scripts dir (pre-zone). Prefer zone_scripts_dir()."""
|
|
59
|
+
return workspace_dir() / "scripts"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def zone_dir(connection: str) -> Path:
|
|
63
|
+
"""Per-connection isolated zone root: agent-workspace/zones/<connection>/.
|
|
64
|
+
|
|
65
|
+
Mirrors browser-harness's domain-skills/<host>/ — one isolated area per
|
|
66
|
+
DSN. `connection` is validated to be a simple identifier (no path parts).
|
|
67
|
+
"""
|
|
68
|
+
_validate_zone_name(connection)
|
|
69
|
+
return workspace_dir() / "zones" / connection
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def zone_scripts_dir(connection: str) -> Path:
|
|
73
|
+
return zone_dir(connection) / "scripts"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def zone_skills_dir(connection: str) -> Path:
|
|
77
|
+
return zone_dir(connection) / "skills"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def zone_helpers_file(connection: str) -> Path:
|
|
81
|
+
"""Per-connection helpers.py, merged over the shared base on use_workspace."""
|
|
82
|
+
return zone_dir(connection) / "helpers.py"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_zone_name(name: str) -> None:
|
|
86
|
+
"""A zone name is a connection name; reject path traversal / odd chars."""
|
|
87
|
+
if not name or "/" in name or "\\" in name or name.startswith("."):
|
|
88
|
+
raise ValueError(f"invalid zone/connection name: {name!r}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def runtime_dir() -> Path:
|
|
92
|
+
"""Runtime state: logs, lock files."""
|
|
93
|
+
override = os.environ.get("BH_SQL_RUNTIME_DIR")
|
|
94
|
+
return Path(override).expanduser() if override else home_dir() / "runtime"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def tmp_dir() -> Path:
|
|
98
|
+
"""Per-session scratch files (query logs, dumps)."""
|
|
99
|
+
override = os.environ.get("BH_SQL_TMP_DIR")
|
|
100
|
+
return Path(override).expanduser() if override else home_dir() / "tmp"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ensure_private_dir(p: Path) -> Path:
|
|
104
|
+
"""Create directory (parents=True) and tighten perms on POSIX."""
|
|
105
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
if not IS_WINDOWS:
|
|
107
|
+
# Best-effort: only tighten on creation
|
|
108
|
+
try:
|
|
109
|
+
p.chmod(0o700)
|
|
110
|
+
except (PermissionError, OSError):
|
|
111
|
+
pass
|
|
112
|
+
return p
|
sql_harness/run.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""sql-harness CLI / heredoc entry point.
|
|
2
|
+
|
|
3
|
+
Mirrors browser-harness's run.py:
|
|
4
|
+
|
|
5
|
+
- Without args + non-TTY stdin: read heredoc body, exec in helpers' globals.
|
|
6
|
+
- Otherwise: dispatch to cli.main().
|
|
7
|
+
|
|
8
|
+
The console script `sql-harness` resolves to this module's `main()`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from . import cli, helpers
|
|
16
|
+
from .agent_loader import load_agent_helpers
|
|
17
|
+
from .config import load as load_config
|
|
18
|
+
from .helpers import set_active
|
|
19
|
+
from .manager import SqlHarness
|
|
20
|
+
from .paths import config_file, ensure_private_dir, home_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Merge every helper into this module's globals so `exec(code, globals())`
|
|
24
|
+
# in heredoc mode sees them as bare names. Mirrors browser-harness's
|
|
25
|
+
# `from .helpers import *` pattern.
|
|
26
|
+
for _name, _value in vars(helpers).items():
|
|
27
|
+
if _name.startswith("_"):
|
|
28
|
+
continue
|
|
29
|
+
globals()[_name] = _value
|
|
30
|
+
|
|
31
|
+
# Mirror the same names for `from .run import *` users.
|
|
32
|
+
__all__ = [n for n in globals().keys() if not n.startswith("_")]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_harness() -> SqlHarness:
|
|
36
|
+
"""Build a SqlHarness from the user's config file."""
|
|
37
|
+
cfg_path = config_file()
|
|
38
|
+
if not cfg_path.exists():
|
|
39
|
+
# First run: create a fresh state dir + empty config file.
|
|
40
|
+
ensure_private_dir(home_dir())
|
|
41
|
+
cfg = load_config(cfg_path)
|
|
42
|
+
else:
|
|
43
|
+
cfg = load_config(cfg_path)
|
|
44
|
+
return SqlHarness(cfg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> int:
|
|
48
|
+
"""Entry point: branch on TTY vs heredoc."""
|
|
49
|
+
harness = _build_harness()
|
|
50
|
+
set_active(harness)
|
|
51
|
+
|
|
52
|
+
# Merge agent_helpers.py into our globals so user-defined helpers
|
|
53
|
+
# are also visible in heredoc mode.
|
|
54
|
+
load_agent_helpers(globals())
|
|
55
|
+
|
|
56
|
+
# Heredoc mode: no args + piped stdin.
|
|
57
|
+
if len(sys.argv) == 1 and not sys.stdin.isatty():
|
|
58
|
+
# Read stdin as UTF-8 on Windows (default locale is GBK/CP936, which
|
|
59
|
+
# mangles non-ASCII bytes into surrogates that exec() rejects).
|
|
60
|
+
# Mirrors browser-harness's Windows stdin handling.
|
|
61
|
+
try:
|
|
62
|
+
sys.stdin.reconfigure(encoding="utf-8", errors="replace")
|
|
63
|
+
except (AttributeError, OSError):
|
|
64
|
+
pass
|
|
65
|
+
code = sys.stdin.read()
|
|
66
|
+
try:
|
|
67
|
+
exec(code, globals())
|
|
68
|
+
return 0
|
|
69
|
+
except SystemExit as e:
|
|
70
|
+
return int(e.code) if e.code is not None else 0
|
|
71
|
+
except Exception:
|
|
72
|
+
import traceback
|
|
73
|
+
|
|
74
|
+
traceback.print_exc()
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
# Otherwise: dispatch to the CLI.
|
|
78
|
+
try:
|
|
79
|
+
return cli.main(sys.argv[1:])
|
|
80
|
+
finally:
|
|
81
|
+
harness.close_all()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
sys.exit(main())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sql-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace.
|
|
5
|
+
Project-URL: Source, https://github.com/zhaoliuxue/much_bigpy/tree/master/lab/sql_harness
|
|
6
|
+
Project-URL: Issues, https://github.com/zhaoliuxue/much_bigpy/issues
|
|
7
|
+
Author: much-bigpy
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agent,cli,database,heredoc,mysql,postgres,sql,ssh
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Database
|
|
16
|
+
Classifier: Topic :: System :: Shells
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: paramiko<4,>=3.4
|
|
19
|
+
Requires-Dist: psycopg[binary]<4,>=3.2
|
|
20
|
+
Requires-Dist: pymysql<2,>=1.1
|
|
21
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# sql-harness
|
|
25
|
+
|
|
26
|
+
A thin, single-process SQL CLI for LLM agents. Mirrors [browser-harness](https://github.com/browser-use/browser-harness)'s structure but targets relational databases (Postgres, MySQL, Redis-soon).
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 1. Install deps (one-time)
|
|
32
|
+
uv sync
|
|
33
|
+
|
|
34
|
+
# 2. Scaffold a starter connections.toml
|
|
35
|
+
uv run sql-harness init
|
|
36
|
+
|
|
37
|
+
# 3. Add a connection
|
|
38
|
+
uv run sql-harness add local_pg --driver postgres --url 'postgresql://postgres:postgres@localhost:5432/postgres'
|
|
39
|
+
|
|
40
|
+
# 4. Test it
|
|
41
|
+
uv run sql-harness test local_pg
|
|
42
|
+
|
|
43
|
+
# 5. Use it
|
|
44
|
+
uv run sql-harness <<'PY'
|
|
45
|
+
use_workspace("local_pg")
|
|
46
|
+
print(query("SELECT version()"))
|
|
47
|
+
print(list_tables())
|
|
48
|
+
print(describe("pg_class"))
|
|
49
|
+
PY
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Architecture (~1k lines across 8 core files)
|
|
53
|
+
|
|
54
|
+
- `install.md` — first-time install + first connection
|
|
55
|
+
- `SKILL.md` — day-to-day usage
|
|
56
|
+
- `lab/sql_harness/src/sql_harness/` — protected core package (src-layout, mirrors browser-harness)
|
|
57
|
+
- `${XDG_CONFIG_HOME:-~/.config}/sql-harness/connections.toml` — plaintext credentials in ONE place
|
|
58
|
+
- `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/agent_helpers.py` — task-specific helpers
|
|
59
|
+
- `${XDG_CONFIG_HOME:-~/.config}/sql-harness/agent-workspace/skills/` — per-task/per-table skills
|
|
60
|
+
|
|
61
|
+
## Why mirror browser-harness?
|
|
62
|
+
|
|
63
|
+
- Same operational ergonomics for agents: heredoc CLI with auto-imported helpers.
|
|
64
|
+
- Same agent-workspace pattern (helpers + skills stay editable per-project).
|
|
65
|
+
- Same path/state layout (XDG-style state directory).
|
|
66
|
+
- Same doc layering: `SKILL.md` body + `interaction-skills/*.md` for stuck points.
|
|
67
|
+
|
|
68
|
+
## What this is NOT
|
|
69
|
+
|
|
70
|
+
- Not a database migration framework.
|
|
71
|
+
- Not an ORM.
|
|
72
|
+
- Not a connection-string parser (`pgcli`/`mycli` already exist for interactive REPL use; this is for *agents*).
|
|
73
|
+
- Not a long-running daemon. SQL connections don't need one.
|
|
74
|
+
|
|
75
|
+
## Connection file format
|
|
76
|
+
|
|
77
|
+
Default: `${XDG_CONFIG_HOME:-~/.config}/sql-harness/connections.toml`.
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
default_workspace = "local_pg"
|
|
81
|
+
|
|
82
|
+
[pool_defaults]
|
|
83
|
+
size = 5
|
|
84
|
+
recycle = 3600
|
|
85
|
+
pre_ping = true
|
|
86
|
+
echo = false
|
|
87
|
+
|
|
88
|
+
[[connections]]
|
|
89
|
+
name = "local_pg"
|
|
90
|
+
driver = "postgres"
|
|
91
|
+
url = "postgresql+psycopg://postgres:postgres@localhost:5432/postgres"
|
|
92
|
+
description = "Local PostgreSQL for dev"
|
|
93
|
+
|
|
94
|
+
[[connections]]
|
|
95
|
+
name = "prod_pg"
|
|
96
|
+
driver = "postgres"
|
|
97
|
+
url = "${env:PROD_PG_URL}" # env-var indirection
|
|
98
|
+
pool_size = 10
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Use `${env:VAR}` placeholders inside `url` for prod secrets — they're expanded at load time from the process environment.
|
|
102
|
+
|
|
103
|
+
## Configuration env vars
|
|
104
|
+
|
|
105
|
+
| Var | Purpose |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `BH_SQL_HOME` | Override state root (`~/.config/sql-harness` by default) |
|
|
108
|
+
| `BH_SQL_CONFIG_FILE` | Override connections.toml path |
|
|
109
|
+
| `BH_SQL_AGENT_WORKSPACE` | Override agent-workspace directory |
|
|
110
|
+
| `BH_SQL_RUNTIME_DIR` | Override runtime state dir |
|
|
111
|
+
| `BH_SQL_TMP_DIR` | Override temp dir |
|
|
112
|
+
| `BH_PG_URL` | Used by integration tests (skipped if unset) |
|
|
113
|
+
| `BH_MYSQL_URL` | Used by integration tests (skipped if unset) |
|
|
114
|
+
|
|
115
|
+
## Drivers
|
|
116
|
+
|
|
117
|
+
| Driver | Backend | Package | Status |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `postgres` | PostgreSQL | `psycopg[binary]>=3.2` | ✅ |
|
|
120
|
+
| `mysql` | MySQL | `pymysql>=1.1` | ✅ |
|
|
121
|
+
| `redis` | Redis | `redis>=5.0` (not yet added) | 🚧 stub |
|
|
122
|
+
| `sqlite` | SQLite | stdlib (`sqlite3`) | ✅ (test-only) |
|
|
123
|
+
| `ssh` / `ssh+password` / `ssh+key` | remote shell + SFTP (paramiko) | `paramiko>=3.4` | ✅ |
|
|
124
|
+
|
|
125
|
+
To enable Redis: `uv add "redis>=5.0,<6"` then implement `drivers/redis.py`.
|
|
126
|
+
To add another SSH host: append a `[[connections]]` block with `driver = "ssh"` and an `ssh://...` URL.
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
PRs and improvements welcome. See `AGENTS.md` for code priorities.
|
|
131
|
+
|
|
132
|
+
- **Skills are written by the harness, not by you.** When you figure out a non-obvious SQL flow (a weird schema, a slow query, a JSON column trick), file a skill in `agent-workspace/skills/<name>.md`. Future sessions will read it before re-discovering it.
|
|
133
|
+
- Bug fixes, new drivers, helper additions all welcome.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
sql_harness/SKILL.md,sha256=O7xnojcL_Q_c8Hnf1TXwUEvbCpEkbD52rfwFDDxA_gM,3258
|
|
2
|
+
sql_harness/__init__.py,sha256=nT5ZKMOTeza9K1inWRHsR3h8ikvK1icekBbkOfX4U_U,669
|
|
3
|
+
sql_harness/agent_loader.py,sha256=CO0MahpXR1jzwQWSrrPGGo_AJ40ogunASP5gZFmjsu0,1821
|
|
4
|
+
sql_harness/cli.py,sha256=OBoB0icCi8dUrqo8RI4aE9bvxwxzwg7euGJWvEbNJSs,30859
|
|
5
|
+
sql_harness/config.py,sha256=CcOTASoyoqhRlTN-a8zY6D1QiK-XJlJ1dv6INWDiwfI,8141
|
|
6
|
+
sql_harness/helpers.py,sha256=bv3rUWeVBcIDdtrn6ZLcubKvkU9MtJjQtuboACkxrNQ,29484
|
|
7
|
+
sql_harness/manager.py,sha256=56iTMTUcqfi_Cktv8qbIU_7W70OP-6T5u_U66RntUaY,3929
|
|
8
|
+
sql_harness/paths.py,sha256=V05o-OPKkxU8kERgrWY3RuIre6xP-AM8CT3EPbY-3Ag,3798
|
|
9
|
+
sql_harness/run.py,sha256=xoAsPEXT-5l0eZwVXMlufCRxl1woD0sZSgBZgzWlzIc,2545
|
|
10
|
+
sql_harness/drivers/__init__.py,sha256=WdxI10lF_gkhFib6CPe2KA6to4jxr_Rmio3-jFsrW3g,1686
|
|
11
|
+
sql_harness/drivers/mysql.py,sha256=W2GH20JZE6mnPGwEIF5zykp31WaNoKx96c4e92LD5kM,1497
|
|
12
|
+
sql_harness/drivers/postgres.py,sha256=dEMBOTFprjYWYY2OreMzi0P8nosk6cFfuSKQhOqT2iY,2190
|
|
13
|
+
sql_harness/drivers/redis.py,sha256=Oywm-sVBFnrXEybMFxkFy4jKL9S2k3kITgGeI1T_RX8,1093
|
|
14
|
+
sql_harness/drivers/sqlite.py,sha256=AzlnccquIJ7XfyLBB6jTFtFMpij_rbHR4e_rr7oxhX8,1358
|
|
15
|
+
sql_harness/drivers/ssh.py,sha256=S5-lEJGihDvqu3BTqUGg6rUoDPOw0h23krtCgBMUlGE,10076
|
|
16
|
+
sql_harness-0.1.0.dist-info/METADATA,sha256=b3tN1nb6tTqNcQzO2macw8HTSztXht02mdTsPNQa3Qg,4895
|
|
17
|
+
sql_harness-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
18
|
+
sql_harness-0.1.0.dist-info/entry_points.txt,sha256=mY6NkQIlefGk1xDAIHcFdPc5jTY-Vi_iUuCBY_2NVTk,53
|
|
19
|
+
sql_harness-0.1.0.dist-info/RECORD,,
|