moshcode 0.89.0 → 0.91.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/README.md CHANGED
@@ -35,6 +35,7 @@ or miss one that does. A test fails the build when it drifts.
35
35
  | `moshcode kill` | runtime | end a herd session |
36
36
  | `moshcode wait` | runtime | block until a session is blocked, done, or idle |
37
37
  | `moshcode restore` | runtime | rebuild the herd's sessions after a reboot |
38
+ | `moshcode ssh` | runtime | persistent SSH workspaces — one connection, many clean commands |
38
39
  | `moshcode install` | engines | install an engine or workflow tool |
39
40
  | `moshcode uninstall` <br>`remove` | engines | take an engine or workflow tool off this machine |
40
41
  | `moshcode upgrade` <br>`update` | engines | update moshcode, engines, or tools |
@@ -579,6 +580,96 @@ current pit untouched.
579
580
  The modes are not identical across providers. In particular, OpenCode `--auto`
580
581
  auto-approves permission requests but continues to enforce explicit deny rules.
581
582
 
583
+ ## SSH workspaces
584
+
585
+ `/ssh` keeps the SSH connection alive; `ssh exec` still gives each tool call a
586
+ clean command channel.
587
+
588
+ A coding run against a remote box is a few hundred small operations: read a
589
+ file, `git status`, apply a patch, run the tests. Each one as a fresh `ssh
590
+ user@host cmd` pays for a TCP handshake, a key exchange, a host-key check and
591
+ authentication every time. OpenSSH can carry many channels over one
592
+ authenticated connection, and `moshcode ssh` is a thin, careful wrapper over
593
+ exactly that — named targets, one persistent master connection per target,
594
+ and a `--json` execution surface built for agents.
595
+
596
+ ```sh
597
+ moshcode ssh add dev deploy@example.com --cwd /srv/app # or an alias from ~/.ssh/config
598
+ moshcode ssh open dev # authenticate once
599
+ moshcode ssh exec dev -- git status --short # …then every command reuses it
600
+ moshcode ssh dev # a real shell, same connection
601
+ moshcode ssh close dev # or let it expire (--persist, default 10m)
602
+ ```
603
+
604
+ Nothing secret is stored. `~/.moshcode/ssh/targets.json` holds a host, a port
605
+ and a directory; your `~/.ssh/config`, agent, `known_hosts`, `ProxyJump` and
606
+ hardware keys keep working exactly as they do at the prompt, and host keys are
607
+ never auto-accepted. The connection lives in an OpenSSH ControlMaster behind a
608
+ socket only you can read, is checked with `ssh -O check` and closed with `ssh
609
+ -O exit`, and a socket the master has gone away from is cleaned up and reopened
610
+ on the next command.
611
+
612
+ ### For an agent: one connection, many clean commands
613
+
614
+ `exec` runs each command on its own channel with no PTY, so stdout, stderr and
615
+ the exit status come back separately and stdin stays raw. `ok` is the
616
+ command's verdict; `transportOk` is ssh's. A `grep` that finds nothing is
617
+ `{ ok: false, transportOk: true, code: 1 }` — a fact about the files, not the
618
+ network.
619
+
620
+ ```sh
621
+ moshcode ssh exec dev --json -- git diff --stat
622
+ # {
623
+ # "ok": true, "target": "dev", "connected": true, "transportOk": true,
624
+ # "code": 0, "signal": null,
625
+ # "stdout": " src/app.ts | 12 +++++---\n", "stderr": "", "durationMs": 14
626
+ # }
627
+
628
+ # a model-produced multi-file patch, applied in one round trip
629
+ git diff | moshcode ssh exec dev --json --stdin --cwd /srv/app -- git apply -
630
+
631
+ moshcode ssh exec dev --json --timeout 10m -- pnpm test
632
+ moshcode ssh exec dev --env NODE_ENV=test -- pnpm test # for this command only
633
+ moshcode ssh exec dev --sh 'git log --oneline | head -5' # a pipeline, on purpose
634
+ ```
635
+
636
+ There is no shell state between calls, deliberately: `exec dev -- cd /tmp`
637
+ followed by `exec dev -- pwd` still answers with the target's cwd. Independent
638
+ commands may run concurrently over the same connection. A run that performs a
639
+ hundred operations authenticates once.
640
+
641
+ The same objects come back from moshscript:
642
+
643
+ ```js
644
+ sshOpen("dev");
645
+ const r = sshExec("dev", ["git", "status", "--short"], { cwd: "/srv/app" });
646
+ if (!r.ok) say(r.stderr);
647
+ sshExec("dev", ["git", "apply", "-"], { cwd: "/srv/app", stdin: patch });
648
+ sshClose("dev");
649
+ ```
650
+
651
+ ### When shell state matters
652
+
653
+ Some work needs a shell that remembers: a `cd`, an export, a dev server, a
654
+ REPL. `shell` puts one in tmux on the remote box, where it outlives this
655
+ terminal, this connection, and the laptop lid.
656
+
657
+ ```sh
658
+ moshcode ssh shell dev --name app # create or attach · Ctrl-b d leaves it running
659
+ moshcode ssh shell send dev/app "pnpm dev" # type into it without attaching
660
+ moshcode ssh shell read dev/app --lines 40 # its screen, as text
661
+ moshcode ssh shell kill dev/app
662
+ ```
663
+
664
+ If tmux is not on the remote box, `shell` says so and `exec` keeps working;
665
+ nothing is installed remotely on your behalf.
666
+
667
+ `put` and `get` copy single files over the same connection with `scp`; `put`
668
+ lands as a temp file and is renamed into place. `bench <name>` measures fresh
669
+ connections against the shared one on your own hosts — on a loopback sshd the
670
+ median command went from ~96ms to ~12ms, and on a real network the handshake
671
+ is the part that grows.
672
+
582
673
  ## Workflow tools: UGig, CoinPay, and the cloud CLIs
583
674
 
584
675
  These remain independent native CLIs with their own authentication,
package/bin/moshcode.mjs CHANGED
@@ -353,6 +353,13 @@ async function main() {
353
353
  process.exitCode = (await herdCommand([cmd === "usage" ? "cost" : cmd, ...rest])) || 0;
354
354
  return;
355
355
  }
356
+ // SSH workspaces (PRD 0013). Imported here rather than at the top: it is a
357
+ // registry read and a few spawns, and `moshcode claude` never needs it.
358
+ if (cmd === "ssh") {
359
+ const { sshCommand } = await import("../src/ssh.mjs");
360
+ process.exitCode = (await sshCommand(rest)) || 0;
361
+ return;
362
+ }
356
363
  if (cmd === "tools") {
357
364
  const asJson = rest.includes("--json");
358
365
  printStatus(toolStatus(), asJson);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.89.0",
3
+ "version": "0.91.0",
4
4
  "type": "module",
5
5
  "description": "moshcode \u2014 a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {