+1
-1
| { | ||
| "name": "moshcode", | ||
| "version": "0.74.0", | ||
| "version": "0.75.0", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "description": "moshcode \u2014 a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript", |
+7
-1
@@ -22,2 +22,3 @@ // Tracked time × the rate they agreed to = the number you send them. | ||
| import { clientLabel, parseFields, resolveClient } from "./clients.mjs"; | ||
| import { captureSpec } from "./pty.mjs"; | ||
| import { GATEWAYS, defaultGateway, gatewayState } from "./payments.mjs"; | ||
@@ -293,3 +294,8 @@ import { chargeFor, describeRate, formatMoney, isDollarPegged, isFiat, rateFor } from "./rates.mjs"; | ||
| const result = run("coinpay", args, { stdio: "inherit" }); | ||
| // Mirrored like every other hand-off: sending an invoice is exactly the kind | ||
| // of thing you want to read back from the session page afterwards. | ||
| const launch = captureSpec({ cmd: "coinpay", args }); | ||
| let result; | ||
| try { result = run(launch.cmd, launch.args, { stdio: "inherit" }); } | ||
| finally { launch.stop(); } | ||
| if (result?.error) { write(err(String(result.error.message || result.error))); return 1; } | ||
@@ -296,0 +302,0 @@ if (result?.status) { write(err(`coinpay exited ${result.status} — invoice ${record.id} is still a local draft`)); return result.status; } |
+10
-1
@@ -25,2 +25,3 @@ // The moshscript command vocabulary — the verbs a .mosh script can call. | ||
| import { shellInvocation } from "./shell.mjs"; | ||
| import { captureSpec } from "./pty.mjs"; | ||
| import { identity, loginAuto, logout as forgetCreds } from "./auth.mjs"; | ||
@@ -120,3 +121,11 @@ import { expandAlias, getAlias, loadAliases, removeAlias, setAlias } from "./aliases.mjs"; | ||
| ctx.out(` ▶ shell: ${cmd}`); | ||
| const res = spawnSync(sh, shArgs, { stdio: "inherit" }); | ||
| // Captured for the session mirror like the pit's own `!cmd`. A blocking | ||
| // spawn holds the event loop, so the follower's poll never runs and the | ||
| // whole command arrives in the drain stop() does — batched rather than | ||
| // live, which is still the difference between reading it from a phone and | ||
| // not. | ||
| const launch = captureSpec({ cmd: sh, args: shArgs }); | ||
| let res; | ||
| try { res = spawnSync(launch.cmd, launch.args, { stdio: "inherit" }); } | ||
| finally { launch.stop(); } | ||
| if (res.error) throw res.error; | ||
@@ -123,0 +132,0 @@ const code = res.status ?? 1; |
+17
-36
@@ -35,7 +35,7 @@ // Agentic-coding engines moshcode can install + wrap. `moshcode install <name>` | ||
| import { spawn } from "node:child_process"; | ||
| import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; | ||
| import { homedir, tmpdir } from "node:os"; | ||
| import { existsSync, readFileSync, statSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import path from "node:path"; | ||
| import { followFile, ptyEnabled, ptySpec, scriptFlavor, stripScriptBanner } from "./pty.mjs"; | ||
| import { captureSpec } from "./pty.mjs"; | ||
@@ -443,4 +443,12 @@ export const ENGINES = { | ||
| const stdio = capture ? ["inherit", "pipe", "pipe"] : "inherit"; | ||
| try { child = spawn(spec.cmd, spec.args, { stdio }); } | ||
| catch (e) { resolve({ ok: false, error: e }); return; } | ||
| // The `capture` branch already reaches a watching browser: it re-writes | ||
| // every byte through this process's own stdout/stderr, which the mirror | ||
| // tees. The inherited branch does not — those bytes go to the tty and | ||
| // nowhere else — so it goes under a pty when a mirror is live. This is what | ||
| // an upgrade, a plugin install and an `mcp add` all run through, and all | ||
| // three used to be a rule, a blank stretch, and a result line. | ||
| const launch = capture ? { ...spec, stop: () => {} } : captureSpec(spec); | ||
| const finish = (result) => { try { launch.stop(); } catch { /* already drained */ } resolve(result); }; | ||
| try { child = spawn(launch.cmd, launch.args, { stdio }); } | ||
| catch (e) { finish({ ok: false, error: e }); return; } | ||
| let output = ""; | ||
@@ -452,3 +460,3 @@ if (capture) { | ||
| } | ||
| child.on("error", (e) => resolve({ ok: false, error: e, output })); | ||
| child.on("error", (e) => finish({ ok: false, error: e, output })); | ||
| // "exit" fires as soon as the process is gone, which with pipes can leave | ||
@@ -459,3 +467,3 @@ // the last chunk still queued — the one line we are trying to read. "close" | ||
| // distinction is kept explicit so neither branch changes by accident. | ||
| child.on(capture ? "close" : "exit", (code, signal) => resolve({ ok: true, code, signal, output })); | ||
| child.on(capture ? "close" : "exit", (code, signal) => finish({ ok: true, code, signal, output })); | ||
| }); | ||
@@ -508,32 +516,5 @@ } | ||
| // a pipe or node-pty. | ||
| let transcript = null; | ||
| let workDir = null; | ||
| let stopFollow = null; | ||
| let launch = { ...spec, stdio: "inherit" }; | ||
| if (ptyEnabled(onOutput)) { | ||
| try { | ||
| workDir = mkdtempSync(path.join(tmpdir(), "moshcode-pty-")); | ||
| transcript = path.join(workDir, "transcript"); | ||
| writeFileSync(transcript, ""); | ||
| const wrapped = ptySpec(spec.cmd, spec.args, transcript, scriptFlavor()); | ||
| if (wrapped) { | ||
| launch = { ...wrapped, stdio: "inherit" }; | ||
| let first = true; | ||
| stopFollow = followFile(transcript, (chunk) => { | ||
| const clean = stripScriptBanner(chunk, first); | ||
| first = false; | ||
| if (clean) onOutput(clean); | ||
| }); | ||
| } | ||
| } catch { | ||
| // Capture is a nicety; never let it stop the session from opening. | ||
| transcript = null; | ||
| } | ||
| } | ||
| const launch = captureSpec(spec, onOutput); | ||
| const cleanup = () => launch.stop(); | ||
| const cleanup = () => { | ||
| try { stopFollow?.(); } catch { /* nothing left to drain */ } | ||
| if (workDir) { try { rmSync(workDir, { recursive: true, force: true }); } catch { /* temp dir */ } } | ||
| }; | ||
| let child; | ||
@@ -540,0 +521,0 @@ try { child = spawn(launch.cmd, launch.args, { stdio: "inherit", env }); } |
+22
-0
@@ -59,2 +59,24 @@ // Live session mirror — the CLI half of `/sessions` on app.moshcode.sh. | ||
| // Where a child process's output should be copied while a mirror is watching. | ||
| // | ||
| // Module-level rather than threaded through every call, because "is anyone | ||
| // watching this pit" is one fact about the process and the launchers that need | ||
| // it are scattered: the shell, the installers, the upgrader, the plugin/skill/ | ||
| // MCP hand-offs. Passing it down by hand is what left most of them writing | ||
| // straight to the tty with the session page showing nothing — each new launcher | ||
| // had to remember, and none of them did. src/pty.mjs reads this as its default, | ||
| // so capture is what a launcher gets for free and opting out is the deliberate | ||
| // act. | ||
| let activeSink = null; | ||
| /** Point child capture at this mirror (or null when the pit stops mirroring). */ | ||
| export function setActiveSink(sink) { | ||
| activeSink = typeof sink === "function" ? sink : null; | ||
| } | ||
| /** The sink a child's output should be copied to, or null when unmirrored. */ | ||
| export function activeChildSink() { | ||
| return activeSink; | ||
| } | ||
| export function createMirror({ | ||
@@ -61,0 +83,0 @@ version = "", |
+5
-1
@@ -25,2 +25,3 @@ // The rail an invoice goes out on. | ||
| import { spawnSync } from "node:child_process"; | ||
| import { captureSpec } from "./pty.mjs"; | ||
@@ -182,3 +183,6 @@ import { loadBusiness, updateBusiness } from "./business-store.mjs"; | ||
| write(info(`handing you to ${bone(gateway.bin)} — it owns its own session`)); | ||
| const result = run(gateway.bin, gateway.connect, { stdio: "inherit" }); | ||
| const launch = captureSpec({ cmd: gateway.bin, args: gateway.connect }); | ||
| let result; | ||
| try { result = run(launch.cmd, launch.args, { stdio: "inherit" }); } | ||
| finally { launch.stop(); } | ||
| if (result?.error) { write(err(String(result.error.message || result.error))); return 1; } | ||
@@ -185,0 +189,0 @@ if (result?.status) { |
+58
-1
@@ -22,4 +22,7 @@ // PTY capture for the session mirror. | ||
| import { spawnSync } from "node:child_process"; | ||
| import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs"; | ||
| import { closeSync, existsSync, mkdtempSync, openSync, readSync, rmSync, statSync, writeFileSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import path from "node:path"; | ||
| import { StringDecoder } from "node:string_decoder"; | ||
| import { activeChildSink } from "./mirror.mjs"; | ||
@@ -182,1 +185,55 @@ /** | ||
| } | ||
| /** | ||
| * Wrap a spawn spec so a copy of everything the child prints reaches `onOutput` | ||
| * while the child still owns the real terminal. | ||
| * | ||
| * The whole capture dance in one place — temp transcript, the flavour-specific | ||
| * `script` argv, the follower, the banner strip, the cleanup — because every | ||
| * launcher in the pit needs it, and each one growing its own copy is how a | ||
| * shell command ended up invisible in the mirror while `/agents claude` was | ||
| * captured: both spawn `inherit`, and only one of them had been taught this. | ||
| * | ||
| * `onOutput` defaults to whatever the live mirror is (src/mirror.mjs), so a | ||
| * launcher gets capture without having to know the mirror exists — the reverse | ||
| * of how this started, where each launcher had to be taught separately and only | ||
| * two ever were. Pass `null` to opt a launch out. | ||
| * | ||
| * Returns `{ cmd, args, stop }`. With nothing watching, or on a box with no | ||
| * `script(1)` we can drive, `cmd`/`args` come back exactly as passed in and | ||
| * `stop` is a no-op — the caller spawns what it always spawned. `stop()` must | ||
| * be called once the child exits: it drains the tail of the transcript (the | ||
| * last lines of a command are usually the ones you were waiting for) and | ||
| * removes the temp dir. | ||
| */ | ||
| export function captureSpec({ cmd, args = [] }, onOutput = activeChildSink(), { flavor = scriptFlavor() } = {}) { | ||
| const plain = { cmd, args, stop: () => {} }; | ||
| if (!ptyEnabled(onOutput, flavor)) return plain; | ||
| let workDir = null; | ||
| try { | ||
| workDir = mkdtempSync(path.join(tmpdir(), "moshcode-pty-")); | ||
| const transcript = path.join(workDir, "transcript"); | ||
| writeFileSync(transcript, ""); | ||
| const wrapped = ptySpec(cmd, args, transcript, flavor); | ||
| if (!wrapped) throw new Error("no script(1) spec for this flavour"); | ||
| let first = true; | ||
| const stopFollow = followFile(transcript, (chunk) => { | ||
| const clean = stripScriptBanner(chunk, first); | ||
| first = false; | ||
| if (clean) onOutput(clean); | ||
| }); | ||
| const dir = workDir; | ||
| return { | ||
| cmd: wrapped.cmd, | ||
| args: wrapped.args, | ||
| stop() { | ||
| try { stopFollow(); } catch { /* nothing left to drain */ } | ||
| try { rmSync(dir, { recursive: true, force: true }); } catch { /* temp dir */ } | ||
| }, | ||
| }; | ||
| } catch { | ||
| // Capture is a nicety; never let it stop a command from running. | ||
| if (workDir) { try { rmSync(workDir, { recursive: true, force: true }); } catch { /* temp dir */ } } | ||
| return plain; | ||
| } | ||
| } |
+31
-8
@@ -21,3 +21,3 @@ // The moshcode shell — run `moshcode` with no args. A metal prompt that opens | ||
| import { loadCommand, saveCommand } from "./settings-sync.mjs"; | ||
| import { createMirror, pressKey, teeOutput } from "./mirror.mjs"; | ||
| import { createMirror, pressKey, setActiveSink, teeOutput } from "./mirror.mjs"; | ||
| import { fetchMotdAd } from "./ads.mjs"; | ||
@@ -32,2 +32,3 @@ import { runScript } from "./runtime.mjs"; | ||
| import { shellInvocation } from "./shell.mjs"; | ||
| import { captureSpec } from "./pty.mjs"; | ||
| import { needsRootHere, primeEscalation } from "./escalate.mjs"; | ||
@@ -683,10 +684,19 @@ import { banner, hr, acid, ash, bone, dim, ok, err, warn, info, moshcodeVersion } from "./ui.mjs"; | ||
| // that is not optional. Resolves { ok, code, signal }. | ||
| function runShell(rawCmd) { | ||
| // | ||
| // Captured through a pty when the mirror is watching, for the same reason an | ||
| // engine is: a shell command is where most of what a pit does actually happens | ||
| // — `!cmd`, /shell, and every shell-valued /alias land here — and with plain | ||
| // `inherit` none of its bytes, on stdout or stderr, ever pass through this | ||
| // process. The session page was showing the echoed command line and the exit | ||
| // note with nothing in between. | ||
| export function runShell(rawCmd, { onOutput } = {}) { | ||
| return new Promise((resolve) => { | ||
| const { shell, args } = shellInvocation(rawCmd); | ||
| const launch = captureSpec({ cmd: shell, args }, onOutput); | ||
| const done = (result) => { try { launch.stop(); } catch { /* already drained */ } resolve(result); }; | ||
| let child; | ||
| try { child = spawn(shell, args, { stdio: "inherit" }); } | ||
| catch (e) { resolve({ ok: false, error: e }); return; } | ||
| child.on("error", (e) => resolve({ ok: false, error: e })); | ||
| child.on("exit", (code, signal) => resolve({ ok: true, code, signal })); | ||
| try { child = spawn(launch.cmd, launch.args, { stdio: "inherit" }); } | ||
| catch (e) { done({ ok: false, error: e }); return; } | ||
| child.on("error", (e) => done({ ok: false, error: e })); | ||
| child.on("exit", (code, signal) => done({ ok: true, code, signal })); | ||
| }); | ||
@@ -706,3 +716,3 @@ } | ||
| console.log(hr()); | ||
| const r = await runShell(rawCmd); | ||
| const r = await runShell(rawCmd, { onOutput: childSink() }); | ||
| console.log(hr()); | ||
@@ -727,4 +737,11 @@ if (!r.ok) { | ||
| console.log(hr()); | ||
| const child = spawn(target.install.cmd, target.install.args, { stdio: "inherit" }); | ||
| // Installers are long, chatty, and the thing you most want to read from a | ||
| // phone — so they go through the mirror's pty like everything else. | ||
| const launch = captureSpec( | ||
| { cmd: target.install.cmd, args: target.install.args }, | ||
| childSink(), | ||
| ); | ||
| const child = spawn(launch.cmd, launch.args, { stdio: "inherit" }); | ||
| child.on("error", (e) => { | ||
| launch.stop(); | ||
| console.log(hr()); | ||
@@ -736,2 +753,3 @@ console.log(err(`install failed: ${e.message}`)); | ||
| child.on("exit", (code) => { | ||
| launch.stop(); | ||
| console.log(hr()); | ||
@@ -1301,2 +1319,6 @@ if (code !== 0) { console.log(err(`install exited ${code}`)); return resolve(); } | ||
| activeMirror = mirror; | ||
| // Every launcher that spawns a child reads this rather than being handed a | ||
| // sink, so a command run from the pit is captured whether or not whoever | ||
| // wrote that launcher knew the mirror existed. | ||
| setActiveSink((chunk) => activeMirror?.write(chunk)); | ||
| const restoreTee = teeOutput((chunk) => mirror.write(chunk)); | ||
@@ -1336,4 +1358,5 @@ | ||
| activeMirror = null; | ||
| setActiveSink(null); | ||
| try { restoreTee?.(); } catch { /* noop */ } | ||
| try { await mirror?.stop(); } catch { /* best effort */ } | ||
| } |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2063107
0.26%36965
0.27%166
0.61%