shmakk 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -33,10 +33,34 @@ export SHMAKK_API_KEY="your-api-key"
33
33
  export SHMAKK_MODEL="gpt-4o-mini"
34
34
  ```
35
35
 
36
- Or copy the template and fill it in:
36
+ Or configure multiple native model providers in `~/.config/shmakk/endpoints.json`:
37
+
38
+ ```json
39
+ {
40
+ "main": "gpt5-codex",
41
+ "models": {
42
+ "gpt5-codex": {
43
+ "provider": "codex",
44
+ "model": "gpt-5-codex",
45
+ "api_key": "OPENAI_API_KEY"
46
+ },
47
+ "local": {
48
+ "provider": "openai-compatible",
49
+ "base_url": "http://127.0.0.1:1234/v1",
50
+ "model": "qwen/qwen3.5-9b"
51
+ },
52
+ "claude": {
53
+ "provider": "anthropic",
54
+ "model": "claude-sonnet-4-5-20250929",
55
+ "api_key": "ANTHROPIC_API_KEY"
56
+ }
57
+ }
58
+ }
59
+ ```
37
60
 
38
61
  ```bash
39
- cp node_modules/shmakk/.env.example .env
62
+ shmakk --endpoint claude
63
+ shmakk --model-recommendation
40
64
  ```
41
65
 
42
66
  ### 2. Launch
@@ -114,7 +138,9 @@ The coordinator system enables complex, multi-step task execution with plan-firs
114
138
  | `SHMAKK_BASE_URL` | OpenAI-compatible base URL |
115
139
  | `SHMAKK_API_KEY` | API key |
116
140
  | `SHMAKK_MODEL` | Default model |
141
+ | `SHMAKK_PROVIDER` | `openai-compatible`, `codex`, or `anthropic` |
117
142
  | `SHMAKK_HEADERS` | Extra headers (k=v,k=v) |
143
+ | `SHMAKK_MODEL_RECOMMENDATION` | Set to `1` to let the configured `main` model route each call |
118
144
 
119
145
  ## Useful commands
120
146
 
@@ -144,6 +170,46 @@ The coordinator system enables complex, multi-step task execution with plan-firs
144
170
  - Secrets (`.env`, keys, tokens) are never sent to the AI
145
171
  - Workspace root is enforced — tools can't access files outside it
146
172
 
173
+ ## Remote SSH
174
+
175
+ The agent can run commands and transfer files on remote hosts via SSH. Configure hosts in `.shmakk/hosts.json` (per-project) or `~/.config/shmakk/hosts.json` (global):
176
+
177
+ ```json
178
+ {
179
+ "hosts": {
180
+ "devbox": {
181
+ "host": "marcus@192.168.1.100",
182
+ "port": 22,
183
+ "auto_approve": false,
184
+ "timeout_sec": 30
185
+ },
186
+ "staging": {
187
+ "host": "deploy@10.0.0.5",
188
+ "port": 2247
189
+ }
190
+ },
191
+ "allow_ssh_config": false,
192
+ "default_timeout_sec": 30
193
+ }
194
+ ```
195
+
196
+ | Tool | Description |
197
+ |------|-------------|
198
+ | `ssh_run` | Run a shell command on a remote host |
199
+ | `ssh_push` | Copy a local workspace file to a remote host |
200
+ | `ssh_pull` | Copy a remote file into the local workspace |
201
+
202
+ SSH key auth via `~/.ssh` is assumed. For persistent connections (avoid re-auth on every call), add to `~/.ssh/config`:
203
+
204
+ ```
205
+ Host *
206
+ ControlMaster auto
207
+ ControlPath ~/.ssh/controlmasters/%r@%h:%p
208
+ ControlPersist 600
209
+ ```
210
+
211
+ Then `mkdir -p ~/.ssh/controlmasters` once.
212
+
147
213
  ## How it works
148
214
 
149
215
  shmakk wraps your shell in a PTY (pseudo-terminal). Every command that fails is checked against a deterministic correction engine (no LLM, no API call). If a correction matches and the fixed command succeeds, shmakk feeds the agent your **original input** (not the fixed command) so the agent can address your full intent — not just the typo. You can also give task instructions in natural language — shmakk uses tools to read files, write code, list directories, and run commands, all constrained to your workspace.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shmakk",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "AI-supervised terminal wrapper — command correction, tool-driven tasks, safety controls",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -48,7 +48,7 @@
48
48
  "node": ">=18"
49
49
  },
50
50
  "dependencies": {
51
- "node-pty": "^1.0.0",
51
+ "@lydell/node-pty": "^1.2.0-beta.12",
52
52
  "openai": "^4.77.0",
53
53
  "wavefile": "^11.0.0"
54
54
  },
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ shmakk demo recorder — uses Playwright to drive the demo HTML page
4
+ while ffmpeg captures the Xvfb display.
5
+
6
+ Usage: python3 scripts/demo/record.py [--out onlymakk.mp4]
7
+ """
8
+
9
+ import subprocess, sys, os, time, argparse, shutil, shlex
10
+ from pathlib import Path
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ SCENES_HTML = Path(__file__).resolve().parent / "scenes.html"
14
+
15
+ FPS = 30
16
+ WIDTH = 1920
17
+ HEIGHT = 1080
18
+ DISPLAY = ":99"
19
+
20
+
21
+ def run(cmd, timeout=120, check=True):
22
+ if isinstance(cmd, str):
23
+ print(f" $ {cmd[:120]}")
24
+ else:
25
+ print(f" $ {' '.join(shlex.quote(str(x)) for x in cmd)[:120]}")
26
+ return subprocess.run(cmd, shell=isinstance(cmd, str),
27
+ capture_output=True, text=True,
28
+ timeout=timeout, check=check)
29
+
30
+
31
+ def is_xvfb_running():
32
+ r = subprocess.run(["pgrep", "-f", f"Xvfb {DISPLAY}"], capture_output=True)
33
+ return r.returncode == 0
34
+
35
+
36
+ def start_xvfb():
37
+ if is_xvfb_running():
38
+ print(f"Xvfb already running on {DISPLAY}")
39
+ return
40
+ print(f"Starting Xvfb on {DISPLAY}...")
41
+ subprocess.Popen(["Xvfb", DISPLAY, "-screen", "0", f"{WIDTH}x{HEIGHT}x24",
42
+ "-ac", "+extension", "RANDR"],
43
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
44
+ time.sleep(1.5)
45
+ if not is_xvfb_running():
46
+ print("ERROR: Xvfb failed to start")
47
+ sys.exit(1)
48
+ print("Xvfb started.")
49
+
50
+
51
+ def record(out_path: Path):
52
+ tmp_dir = ROOT / "tmp" / "demo"
53
+ tmp_dir.mkdir(parents=True, exist_ok=True)
54
+ raw_mp4 = tmp_dir / "raw.mp4"
55
+
56
+ # Clean up previous raw files
57
+ for f in list(tmp_dir.glob("*.webm")) + list(tmp_dir.glob("*.mp4")):
58
+ if f.name != "onlymakk.mp4":
59
+ f.unlink(missing_ok=True)
60
+
61
+ start_xvfb()
62
+
63
+ env = os.environ.copy()
64
+ env["DISPLAY"] = DISPLAY
65
+
66
+ scenes_url = SCENES_HTML.as_uri()
67
+
68
+ print(f"Source: {scenes_url}")
69
+ print(f"Output: {out_path}")
70
+
71
+ # Start ffmpeg recording in the background
72
+ ffmpeg_cmd = [
73
+ "ffmpeg", "-y",
74
+ "-f", "x11grab",
75
+ "-framerate", str(FPS),
76
+ "-video_size", f"{WIDTH}x{HEIGHT}",
77
+ "-i", f"{DISPLAY}+0,0",
78
+ "-c:v", "libx264",
79
+ "-preset", "ultrafast",
80
+ "-crf", "18",
81
+ "-pix_fmt", "yuv420p",
82
+ str(raw_mp4)
83
+ ]
84
+
85
+ print("Starting ffmpeg capture...")
86
+ ffmpeg_proc = subprocess.Popen(ffmpeg_cmd, env=env,
87
+ stdout=subprocess.DEVNULL,
88
+ stderr=subprocess.DEVNULL)
89
+ time.sleep(1) # let ffmpeg initialize
90
+
91
+ if ffmpeg_proc.poll() is not None:
92
+ print("ERROR: ffmpeg exited immediately")
93
+ sys.exit(1)
94
+
95
+ # Now run Playwright to drive the demo
96
+ driver_js = f"""
97
+ const {{ chromium }} = require('playwright');
98
+
99
+ (async () => {{
100
+ const browser = await chromium.launch({{
101
+ headless: false,
102
+ args: ['--no-sandbox', '--disable-gpu', '--disable-setuid-sandbox',
103
+ '--window-size={WIDTH},{HEIGHT}', '--window-position=0,0']
104
+ }});
105
+
106
+ // Make the browser window fill the screen
107
+ const context = await browser.newContext({{
108
+ viewport: {{ width: {WIDTH}, height: {HEIGHT} }},
109
+ deviceScaleFactor: 1,
110
+ }});
111
+ const page = await context.newPage();
112
+
113
+ await page.goto('{scenes_url}', {{ waitUntil: 'networkidle' }});
114
+
115
+ // Wait for DONE signal or timeout
116
+ const startTime = Date.now();
117
+ const maxDuration = 120_000;
118
+ while (true) {{
119
+ const title = await page.title();
120
+ if (title === 'DONE' || (Date.now() - startTime) > maxDuration) break;
121
+ await new Promise(r => setTimeout(r, 500));
122
+ }}
123
+
124
+ console.log('Demo finished. Title:', await page.title());
125
+ console.log('Elapsed:', (Date.now() - startTime) / 1000, 's');
126
+
127
+ // Hold the last frame briefly
128
+ await new Promise(r => setTimeout(r, 2000));
129
+
130
+ await browser.close();
131
+ console.log('Browser closed.');
132
+ }})();
133
+ """
134
+
135
+ driver_script = tmp_dir / "driver.js"
136
+ driver_script.write_text(driver_js)
137
+
138
+ print("Launching demo driver...")
139
+ result = subprocess.run(
140
+ ["node", str(driver_script)],
141
+ env=env,
142
+ capture_output=True, text=True,
143
+ timeout=150,
144
+ cwd=str(ROOT)
145
+ )
146
+ print(result.stdout)
147
+ if result.returncode != 0:
148
+ print("STDERR:", result.stderr)
149
+ ffmpeg_proc.terminate()
150
+ ffmpeg_proc.wait()
151
+ sys.exit(1)
152
+
153
+ # Give ffmpeg time to flush
154
+ time.sleep(2)
155
+ ffmpeg_proc.terminate()
156
+ try:
157
+ ffmpeg_proc.wait(timeout=10)
158
+ except subprocess.TimeoutExpired:
159
+ ffmpeg_proc.kill()
160
+ ffmpeg_proc.wait()
161
+
162
+ print(f"Raw capture: {raw_mp4.stat().st_size / 1024 / 1024:.1f} MB")
163
+
164
+ # Re-encode for optimal size and faststart
165
+ final_tmp = tmp_dir / "final.mp4"
166
+ run([
167
+ "ffmpeg", "-y",
168
+ "-i", str(raw_mp4),
169
+ "-c:v", "libx264",
170
+ "-preset", "medium",
171
+ "-crf", "20",
172
+ "-pix_fmt", "yuv420p",
173
+ "-movflags", "+faststart",
174
+ str(final_tmp)
175
+ ], timeout=300)
176
+
177
+ # Move to final destination
178
+ shutil.move(str(final_tmp), str(out_path))
179
+
180
+ # Cleanup
181
+ driver_script.unlink(missing_ok=True)
182
+ raw_mp4.unlink(missing_ok=True)
183
+
184
+ print(f"\nDone! -> {out_path} ({out_path.stat().st_size / 1024 / 1024:.1f} MB)")
185
+
186
+
187
+ def main():
188
+ parser = argparse.ArgumentParser(description="Record shmakk product demo")
189
+ parser.add_argument("--out", default=str(ROOT / "onlymakk.mp4"),
190
+ help="Output MP4 path")
191
+ args = parser.parse_args()
192
+ record(Path(args.out))
193
+
194
+
195
+ if __name__ == "__main__":
196
+ main()