moshcode 0.90.0 → 0.92.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,
@@ -919,6 +1010,39 @@ an equity's score, and each response ships the `caveats` that say so. Prices are
919
1010
  Alpaca's US venue alone and can differ materially from other exchanges. Research
920
1011
  aid, not advice — and like `stocks`, nothing under `crypto` can place an order.
921
1012
 
1013
+ ### Throttle (`/nice`)
1014
+
1015
+ The pit's job is starting other people's programs, and some of them are not shy.
1016
+ Run a few engines at once on a box you also want to type on and you get the
1017
+ failure everyone knows: nothing crashed, but the machine stops answering.
1018
+
1019
+ ```text
1020
+ /nice on # nice -n10 + ionice -c2 -n7 for every engine started after
1021
+ /nice mem 2G # a ceiling, so a runaway dies alone
1022
+ /nice cpu 15 # yield more (nice takes -20..19)
1023
+ /nice # what it is set to
1024
+ /nice off # back to normal priority (the default)
1025
+ ```
1026
+
1027
+ It is off by default — a throttle nobody asked for is a slow engine nobody can
1028
+ explain — and it applies to engines started *after* you turn it on.
1029
+
1030
+ **`nice` alone is half a fix, and it is worth knowing which half.** It reorders
1031
+ CPU, so it buys back the part of a freeze you could have waited out. It does
1032
+ nothing about memory, and memory is the stall that actually costs you a session:
1033
+ once free RAM runs out the kernel reclaims, reclaim goes to disk, and no
1034
+ scheduling priority makes that faster. That is why `/nice mem` exists — it puts
1035
+ the engine in a systemd scope with a hard ceiling, so the one runaway process
1036
+ gets killed instead of the whole box going unresponsive.
1037
+
1038
+ The ceiling is the one setting that can silently not apply: `systemd-run --user`
1039
+ needs a systemd user session, and an ssh login without lingering has none.
1040
+ `/nice` says so in its status line rather than pretending, and the CPU and I/O
1041
+ halves still work. On a box with no `nice` or `ionice` at all, and on Windows,
1042
+ the whole thing is a no-op — engines spawn exactly as they did before.
1043
+
1044
+ Settings live in `~/.moshcode/nice.json`, owner-only like the history file.
1045
+
922
1046
  ### Aliases (`/alias`)
923
1047
 
924
1048
  The pit is a prompt you sit at all day, so it lets you name the lines you keep
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.90.0",
3
+ "version": "0.92.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": {