Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 117 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,12 @@ In the TUI shell it's `/prd [idea]`.

## moshscript

A metal scripting toolkit. Paste dead-simple, readable scripts and run them.
A metal scripting toolkit — **secretly all JS is legal**. The simple surface
stays dead-simple, but a `.mosh` file is real JavaScript under the hood with the
full moshcode command vocabulary injected as globals:

```
```js
// alive.mosh — the starter script (unchanged, still works)
while (alive) {
code();
mosh();
Expand All @@ -138,45 +141,139 @@ while (alive) {
} // no bugs, only features
```

## Run
The secret that it's all JS — no new syntax to learn:

```js
// deploy-agents.mosh — real work, still reads like the toy
const engines = ["claude", "codex"];
for (const e of engines) {
install(e); // → moshcode install <e>
}
mcp("install", "https://mcp.sentry.dev/mcp"); // fan out across engines
say(`ready to mosh with ${engines.length} engines`);
agents("claude"); // drop into an autonomous session
```

### Run

```sh
moshcode run examples/alive.mosh # run a script
moshcode run - < script.mosh # or pipe/paste from stdin
moshcode run --max 5 # bound the while loop (default 3)
echo 'say("hi"); notify();' | moshcode run -
moshcode commands # list built-in commands
moshcode help
moshcode run examples/alive.mosh # run a script
moshcode run deploy.mosh --dry-run # narrate without executing
moshcode run alive.mosh --max 5 # bound the while loop (default 3)
moshcode run deploy.mosh staging --fast # extra args reach the script as argv
moshcode run - < script.mosh # pipe/paste from stdin
moshcode commands # list the full vocabulary
```

No install/build step — it's plain ESM. `node bin/moshcode.mjs …` works too.

## moshscript
### Shebang — self-running scripts

`.mosh` files support shebang lines, so `chmod +x` makes them run like shell
scripts. The `moshscript` executable is installed alongside `moshcode`:

```js
#!/usr/bin/env moshscript
// deploy.mosh — chmod +x it and run it like any shell script
install("claude");
agents("claude");
```

The whole language:
```sh
chmod +x deploy.mosh
./deploy.mosh # shebang → moshscript → moshcode run
./deploy.mosh --dry-run staging # args after the file reach the script
```

### Commands

**Local verbs** (moshscript-only, in-process):

| verb | description |
|---|---|
| `code()` | compile features (no bugs) |
| `mosh()` | open the pit + blast the moshcoding playlist |
| `notify(msg)` | fire-and-forget ping + approval link on moshcode.sh |
| `ask(prompt)` | blocking gate — waits for human reply at moshcode.sh |
| `say("…")` | print a line |
| `sleep(ms)` | pause for N milliseconds (blocking) |
| `stop()` | end the loop (`alive = false`) |
| `repeat()` | back to the top of the loop |

**CLI verbs** (each shells out to `moshcode <name> ...args`):

| verb | description |
|---|---|
| `agents(engine)` | launch an autonomous agent session |
| `start(engine)` | raw-launch an engine |
| `install(target)` | install an engine or workflow tool |
| `upgrade(targets…)` | upgrade moshcode, engines, and tools |
| `mcp(args…)` | register/fan out an MCP server |
| `skill(args…)` | install a skill across engines |
| `prd(idea)` | publish/author an OpenPRD doc |
| `ugig(args…)` | drive the ugig workflow CLI |
| `coinpay(args…)` | drive the coinpay workflow CLI |
| `c0mpute(args…)` | drive the c0mpute workflow CLI |
| `pwd()` | print the current repo/location |
| `run(file)` | run another .mosh file (include/compose) |

**Specials** (injected globals, not commands):

| name | description |
|---|---|
| `alive` | `true` while the loop may continue; reads bounded by `--max` |
| `argv` | positional args passed after the script file |
| `env` | `process.env` — parameterize scripts from the environment |

### Human-in-the-loop

- `notify(msg)` — fire-and-forget. Pings the operator across configured channels
and surfaces an approval link at `app.moshcode.sh/approve/:id`. Returns `{ id, url }`.
- `ask(prompt)` — blocking gate. Same ping + link, then **blocks** until the
operator opens the link, reads the context, types instructions, and submits.
Resolves with their text (or `null` on timeout). Use with `await`:

- `while (alive) { … }` — loops the body while the `alive` flag is set (bounded by `--max`).
- `name(args…);` — call a command. `//` comments are ignored.
```js
const task = await ask("what should I work on next?");
say(`got it: ${task}`);
```

Built-in commands: `code()` `mosh()` `notify()` `repeat()` `say("…")` `sleep(ms)` `stop()`.
### Dry run

### notify()
`--dry-run` narrates every action without executing it — no engine spawns, no
installs, no network POSTs, no PRD writes:

Pings **moshcoding.com web notifications**, and — if `MOSHCODE_WEBHOOK_URL` is set —
also POSTs to that webhook. Both are HMAC-signed (`X-Moshcode-Signature`) with
`MOSHCODE_WEBHOOK_SECRET`.
```
$ moshcode run deploy.mosh --dry-run
🎸 moshcode — running moshscript (dry run)

▶ install(claude) → would run: moshcode install claude
▶ mcp(install, https://mcp.sentry.dev/mcp) → would run: moshcode mcp install …
💬 ready to mosh with 2 engines
▶ agents(claude) → would run: moshcode agents claude

✓ 0 loop(s) — no bugs, only features. 🤘
```

### Add your own commands

The vocabulary is open for extension via the registry:

```js
import { defaultCommands } from "moshcode/src/commands.mjs";
const commands = { ...defaultCommands(), deploy: (ctx) => ctx.out("shipping…") };
import { moshVocabulary } from "moshcode/src/commands.mjs";
import { runScript } from "moshcode/src/runtime.mjs";

const commands = moshVocabulary();
commands.register({ name: "deploy", summary: "ship it", run: (ctx) => ctx.out("shipping…") });
await runScript(src, { commands });
```

## Env

| var | default | purpose |
|---|---|---|
| `MOSHCODE_API` | `https://moshcoding.com` | web-notifications endpoint host |
| `MOSHCODE_SITE` | `https://app.moshcode.sh` | approval URL base |
| `MOSHCODE_WEBHOOK_URL` | — | optional extra webhook for `notify()` |
| `MOSHCODE_WEBHOOK_SECRET` | — | signs notify() posts |
| `MOSHCODE_PLAYLIST` | Spotify playlist URL | what `mosh()` blasts in the browser |
29 changes: 22 additions & 7 deletions bin/moshcode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ async function launchEngine(key, engine, args, { agentMode = false } = {}) {
}

function help() {
const vocab = moshVocabulary().all();
const local = vocab.filter((c) => !["run","agents","start","install","upgrade","mcp","skill","prd","ugig","coinpay","c0mpute","pwd"].includes(c.name));
const cli = vocab.filter((c) => !local.includes(c));
console.log(`moshcode — metal scripting toolkit 🤘

usage:
Expand All @@ -103,8 +106,10 @@ usage:
moshcode <engine> [args…] raw launch shorthand (backward compatible)
moshcode <tool> [args…] transparently invoke ugig, coinpay, or c0mpute
moshcode run [file.mosh] [--max N] run a moshscript (stdin with '-', or the
built-in loop if no file); --max bounds
the while loop (default 3)
[--dry-run] [args…] built-in loop if no file); --max bounds
the while loop (default 3); --dry-run
narrates without executing; extra args
reach the script as argv
moshcode mcp install <url> register an MCP server across every engine
moshcode mcp add <name> <url|cmd> that supports it (claude/gemini/codex/opencode)
moshcode skill install <git-url> install a skill across every engine that
Expand All @@ -131,14 +136,24 @@ isolated or trusted workspaces. use \`moshcode start <engine>\` for native defau
tools (native CLI passthrough; each tool owns its auth and output):
${toolList()}

moshscript looks like this:
moshscript — secretly all JS is legal:
${DEFAULT_SCRIPT}
commands: code() mosh() notify() repeat() say("…") sleep(ms) stop()
notify() pings moshcoding.com web notifications, and a webhook too if
MOSHCODE_WEBHOOK_URL is set (signed with MOSHCODE_WEBHOOK_SECRET).
a .mosh file is real JavaScript with the command vocabulary injected as globals.
const, for, if, await, template strings — all just work. shebang lines
(#!/usr/bin/env moshscript) are stripped automatically, so chmod +x works.

local commands (moshscript-only):
${local.map((c) => ` ${(`${c.name}()`).padEnd(14)} ${c.summary}`).join("\n")}

CLI commands (each shells out to \`moshcode <name> ...args\`):
${cli.map((c) => ` ${(`${c.name}()`).padEnd(14)} ${c.summary}`).join("\n")}

human-in-the-loop:
notify(msg) fire-and-forget ping to moshcoding.com + webhook
ask(prompt) blocking gate — waits for human reply at moshcode.sh

env: MOSHCODE_API (default https://moshcoding.com), MOSHCODE_WEBHOOK_URL,
MOSHCODE_WEBHOOK_SECRET
MOSHCODE_WEBHOOK_SECRET, MOSHCODE_PLAYLIST
`);
}

Expand Down
26 changes: 26 additions & 0 deletions bin/moshscript.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node
// moshscript — thin alias for `moshcode run`, so `.mosh` files can use:
//
// #!/usr/bin/env moshscript
//
// as a shebang and run themselves like shell scripts:
//
// chmod +x deploy.mosh && ./deploy.mosh --dry-run staging
//
// All arguments are forwarded unchanged to `moshcode run`.
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";

const BIN = fileURLToPath(new URL("moshcode.mjs", import.meta.url));
const args = process.argv.slice(2); // everything after `moshscript`

const child = spawn(process.execPath, [BIN, "run", ...args], { stdio: "inherit" });
child.on("error", (e) => { console.error(`moshscript: ${e.message}`); process.exit(1); });
child.on("exit", (code, signal) => {
if (signal) {
try { process.kill(process.pid, signal); }
catch { process.exitCode = 1; }
return;
}
process.exitCode = code ?? 0;
});
14 changes: 13 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ INSTALL_URL="https://moshcoding.com/install.sh"
MOSHCODE_HOME="${MOSHCODE_HOME:-$HOME/.moshcode}"
MOSHCODE_BIN="${MOSHCODE_BIN:-$HOME/.local/bin}"
WRAPPER="$MOSHCODE_BIN/moshcode"
SCRIPT_WRAPPER="$MOSHCODE_BIN/moshscript"

# ---- pretty output (acid-lime, matching the CLI) --------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
Expand Down Expand Up @@ -97,6 +98,16 @@ exec node "$MOSHCODE_HOME/bin/moshcode.mjs" "\$@"
WRAP_EOF
chmod +x "$WRAPPER"
ok "wrapper at $WRAPPER"

# moshscript — thin alias for `moshcode run`, so .mosh files can use
# #!/usr/bin/env moshscript as a shebang and run like shell scripts.
cat > "$SCRIPT_WRAPPER" <<SCRIPT_EOF
#!/bin/sh
# moshscript wrapper — installed by $INSTALL_URL. Re-run the installer to update.
exec node "$MOSHCODE_HOME/bin/moshcode.mjs" run "\$@"
SCRIPT_EOF
chmod +x "$SCRIPT_WRAPPER"
ok "wrapper at $SCRIPT_WRAPPER"
}

ensure_path() {
Expand Down Expand Up @@ -130,8 +141,9 @@ run_install() {
run_remove() {
info "removing moshcode"
rm -f "$WRAPPER" 2>/dev/null || true
rm -f "$SCRIPT_WRAPPER" 2>/dev/null || true
rm -rf "$MOSHCODE_HOME" 2>/dev/null || true
ok "removed $WRAPPER and $MOSHCODE_HOME. 🤘"
ok "removed $WRAPPER, $SCRIPT_WRAPPER, and $MOSHCODE_HOME. 🤘"
}

CMD="${1:-install}"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"type": "module",
"description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
"bin": {
"moshcode": "./bin/moshcode.mjs"
"moshcode": "./bin/moshcode.mjs",
"moshscript": "./bin/moshscript.mjs"
},
"scripts": {
"start": "node bin/moshcode.mjs",
Expand Down
2 changes: 1 addition & 1 deletion prd/0004-moshscript-run-programmable-moshcode.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
openprd: "0.2"
id: "0004"
title: moshscript — a scriptable /run for driving all of moshcode programmatically
status: Draft
status: Accepted
authors:
- anthony@chovy.com
created: 2026-07-13
Expand Down
7 changes: 7 additions & 0 deletions src/tui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ function printHelp() {
` ${acid("/help")} this`,
` ${acid("/quit")} leave the pit (or Ctrl-D)`,
"",
bone(" moshscript") + ash(" — secretly all JS is legal"),
ash(" .mosh files are real JavaScript with the command vocabulary injected."),
ash(" local verbs: ") + acid("code() mosh() notify() ask() say() sleep() stop() repeat()"),
ash(" CLI verbs: ") + acid("agents() start() install() upgrade() mcp() skill() prd()"),
ash(" ") + acid("ugig() coinpay() c0mpute() pwd() run()"),
ash(" shebang: ") + acid("#!/usr/bin/env moshscript") + ash(" (chmod +x to self-run)"),
"",
ash(" raw shortcuts: type an engine or tool name by itself, e.g. ") + acid("claude") + ash(" or ") + acid("ugig"),
].join("\n"));
}
Expand Down
58 changes: 58 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import test from "node:test";

import { runMoshcode, cliVerb } from "../src/cli.mjs";
import { moshVocabulary } from "../src/commands.mjs";
import { runScript } from "../src/runtime.mjs";
import { createRegistry } from "../src/registry.mjs";

function dryCtx() {
return { dryRun: true, lines: [], out(l) { this.lines.push(l); } };
Expand All @@ -29,3 +31,59 @@ test("the CLI capabilities are all registered as verbs", () => {
assert.ok(reg.has(name), `expected ${name}() in the vocabulary`);
}
});

// R12: pure unit tests for every CLI verb → argv mapping (dry-run, no real spawns).
// Each case verifies the verb narrates the correct `moshcode <cmd> ...args` argv.
const VERB_ARGV_CASES = [
{ verb: "agents", args: ["claude"], expect: /moshcode agents claude/ },
{ verb: "agents", args: ["opencode", "--model", "gpt-4"], expect: /moshcode agents opencode --model gpt-4/ },
{ verb: "start", args: ["codex", "--sandbox"], expect: /moshcode start codex --sandbox/ },
{ verb: "install", args: ["claude"], expect: /moshcode install claude/ },
{ verb: "install", args: ["ugig"], expect: /moshcode install ugig/ },
{ verb: "upgrade", args: ["self"], expect: /moshcode upgrade self/ },
{ verb: "upgrade", args: [], expect: /moshcode upgrade/ },
{ verb: "mcp", args: ["install", "https://mcp.sentry.dev/mcp"], expect: /moshcode mcp install https:\/\/mcp\.sentry\.dev\/mcp/ },
{ verb: "skill", args: ["install", "https://github.com/example/skill"], expect: /moshcode skill install/ },
{ verb: "prd", args: ["my great idea"], expect: /moshcode prd my great idea/ },
{ verb: "ugig", args: ["--json", "gigs", "list"], expect: /moshcode ugig --json gigs list/ },
{ verb: "coinpay", args: ["wallet", "balance"], expect: /moshcode coinpay wallet balance/ },
{ verb: "c0mpute", args: ["status"], expect: /moshcode c0mpute status/ },
{ verb: "pwd", args: [], expect: /moshcode pwd/ },
{ verb: "run", args: ["setup.mosh"], expect: /moshcode run setup\.mosh/ },
];

for (const { verb, args, expect: pattern } of VERB_ARGV_CASES) {
test(`verb→argv: ${verb}(${args.map(JSON.stringify).join(", ")}) narrates the correct argv`, () => {
const ctx = dryCtx();
const cmd = moshVocabulary().get(verb);
assert.ok(cmd, `${verb}() must be in the vocabulary`);
cmd.run(ctx, ...args);
const output = ctx.lines.join("\n");
assert.match(output, pattern, `expected ${verb}() to narrate ${pattern}, got: ${output}`);
});
}

// Verify CLI verbs return { ok, dryRun } under dry-run (no real spawn).
test("all CLI verbs return { ok: true, dryRun: true } in dry-run mode", () => {
const cliNames = ["agents", "start", "install", "upgrade", "mcp", "skill", "prd", "ugig", "coinpay", "c0mpute", "pwd", "run"];
for (const name of cliNames) {
const ctx = dryCtx();
const cmd = moshVocabulary().get(name);
const result = cmd.run(ctx, "test-arg");
assert.equal(result.ok, true, `${name}() should return ok: true`);
assert.equal(result.dryRun, true, `${name}() should return dryRun: true`);
}
});

// Verify CLI verbs are callable from a real moshscript (dry-run, end-to-end through the runtime).
test("CLI verbs are callable from moshscript in dry-run mode", async () => {
const lines = [];
await runScript(
`install("claude"); agents("claude"); mcp("install", "https://example.com/mcp");`,
{ commands: moshVocabulary(), dryRun: true, out: (s) => lines.push(s) }
);
const output = lines.join("\n");
assert.match(output, /would run: moshcode install claude/);
assert.match(output, /would run: moshcode agents claude/);
assert.match(output, /would run: moshcode mcp install https:\/\/example\.com\/mcp/);
});
Loading