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 +68 -2
- package/package.json +2 -2
- package/scripts/demo/record.py +196 -0
- package/scripts/demo/scenes.html +913 -0
- package/skills/media-video-compose.md +320 -0
- package/skills/media-video-script.md +204 -0
- package/skills/media-video-voice.md +184 -0
- package/src/agent-overview.js +320 -0
- package/src/agent-roster.js +53 -0
- package/src/agent.js +178 -18
- package/src/cli.js +220 -86
- package/src/completions.js +3 -1
- package/src/correction.js +11 -4
- package/src/endpoints.js +94 -31
- package/src/guard.js +101 -0
- package/src/index.js +19 -5
- package/src/llm.js +462 -52
- package/src/markdown.js +217 -0
- package/src/notify.js +34 -0
- package/src/pty.js +1 -1
- package/src/review.js +8 -1
- package/src/self-commands.js +108 -2
- package/src/session.js +58 -2
- package/src/ssh.js +255 -0
- package/src/subagent.js +12 -1
- package/src/taskClassifier.js +2 -2
- package/src/team.js +22 -0
- package/src/tools.js +487 -1
- package/src/workflows.js +32 -0
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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()
|