New:Socket for Asana Is Now Available.Learn more
Get Started

moshcode

Package Overview
Dependencies
Maintainers
1
Versions
60
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

moshcode - npm Package Compare versions

Comparing version
0.74.0
to
0.75.0
+1
-1
package.json
{
"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",

@@ -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; }

@@ -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;

@@ -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 }); }

@@ -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 = "",

@@ -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) {

@@ -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;
}
}

@@ -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 */ }
}