moshcode 0.58.0 → 0.60.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 +299 -6
- package/bin/moshcode.mjs +3 -3
- package/examples/account.mosh +48 -0
- package/examples/aliases.mosh +56 -0
- package/examples/research-desk.mosh +49 -0
- package/package.json +1 -1
- package/prd/0011-herd-agent-protocol.md +391 -0
- package/prd/README.md +1 -0
- package/src/auth.mjs +59 -64
- package/src/cli-schema.mjs +133 -4
- package/src/commands.mjs +389 -39
- package/src/cost-cli.mjs +232 -0
- package/src/cost-pricing.mjs +159 -0
- package/src/cost.mjs +753 -0
- package/src/engines.mjs +32 -0
- package/src/games-breakout.mjs +64 -10
- package/src/games-paddle.mjs +128 -0
- package/src/games-pong.mjs +53 -4
- package/src/games.mjs +164 -12
- package/src/herd-cli.mjs +816 -20
- package/src/herd-eval.mjs +301 -0
- package/src/herd-hooks.mjs +285 -0
- package/src/herd-remote.mjs +365 -0
- package/src/herd-serve.mjs +515 -0
- package/src/herd-state.mjs +167 -10
- package/src/herd-tasks.mjs +377 -0
- package/src/herd.mjs +89 -7
- package/src/templates.mjs +32 -5
- package/src/tools.mjs +43 -0
- package/src/tui.mjs +1 -0
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ or miss one that does. A test fails the build when it drifts.
|
|
|
30
30
|
| `moshcode start` | engines | launch an engine with its native defaults |
|
|
31
31
|
| `moshcode herd` | runtime | run agent sessions that outlive this terminal |
|
|
32
32
|
| `moshcode ps` | runtime | list herd sessions and what each one is doing |
|
|
33
|
+
| `moshcode cost` <br>`usage` | runtime | what each session is spending, read from the engines' own logs |
|
|
33
34
|
| `moshcode attach` | runtime | attach this terminal to a herd session |
|
|
34
35
|
| `moshcode kill` | runtime | end a herd session |
|
|
35
36
|
| `moshcode wait` | runtime | block until a session is blocked, done, or idle |
|
|
@@ -262,9 +263,9 @@ moshcode agents claude -d --name api # and an agent
|
|
|
262
263
|
|
|
263
264
|
```sh
|
|
264
265
|
$ moshcode ps
|
|
265
|
-
api claude blocked ~/src/coinpay 3m
|
|
266
|
-
logs shell idle ~/src/coinpay 3m
|
|
267
|
-
work shell idle ~/src/coinpay 3m
|
|
266
|
+
api claude blocked ~/src/coinpay 3m screen
|
|
267
|
+
logs shell idle ~/src/coinpay 3m screen
|
|
268
|
+
work shell idle ~/src/coinpay 3m screen
|
|
268
269
|
|
|
269
270
|
⚠ 1 waiting on you — moshcode attach api
|
|
270
271
|
```
|
|
@@ -316,9 +317,9 @@ Every session carries a state: `working`, `blocked`, `done`, `idle`, or
|
|
|
316
317
|
`unknown`. `blocked` means a human decision is the only thing missing.
|
|
317
318
|
|
|
318
319
|
```
|
|
319
|
-
api claude blocked ~/src/coinpay 12m
|
|
320
|
-
web codex working ~/src/ugig.net 4m
|
|
321
|
-
audit opencode done ~/src/moshpit-dns 1h
|
|
320
|
+
api claude blocked ~/src/coinpay 12m hook
|
|
321
|
+
web codex working ~/src/ugig.net 4m screen
|
|
322
|
+
audit opencode done ~/src/moshpit-dns 1h runtime
|
|
322
323
|
```
|
|
323
324
|
|
|
324
325
|
State comes from one authority per session, never two. An engine that reports
|
|
@@ -338,6 +339,58 @@ moshcode herd start claude --name watch # then run `moshcode herd watch` in the
|
|
|
338
339
|
|
|
339
340
|
With `--ask`, whatever you reply is typed into the session that was waiting.
|
|
340
341
|
|
|
342
|
+
### What it is costing
|
|
343
|
+
|
|
344
|
+
Every engine already writes down what it used, so nothing has to be
|
|
345
|
+
instrumented or proxied — `moshcode cost` reads the CLIs' own session logs and
|
|
346
|
+
lines them up against the herd:
|
|
347
|
+
|
|
348
|
+
```sh
|
|
349
|
+
moshcode cost # per session, in the window (default: 24h)
|
|
350
|
+
moshcode cost api # one session, with its engine runs
|
|
351
|
+
moshcode cost --all --since 7d # every engine session on the box, herd or not
|
|
352
|
+
moshcode cost --watch # the same report, re-read every 10s
|
|
353
|
+
moshcode cost --json # for a script
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
session engine model in out cache cost age
|
|
358
|
+
api claude claude-opus-5 1.2k 27k 10.5M $9.91~ 42m
|
|
359
|
+
audit codex gpt-5.6-sol 400 200 600 — 12m
|
|
360
|
+
|
|
361
|
+
total $9.91~ 1.6k in · 27k out · 10.5M cached
|
|
362
|
+
~ estimated from published rates; unmarked figures are the engine's own.
|
|
363
|
+
⚠ no rate for gpt-5.6-sol — tokens counted, cost omitted.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
| engine | where the number comes from |
|
|
367
|
+
|---|---|
|
|
368
|
+
| claude | per-message `usage` in `~/.claude/projects/**/*.jsonl` |
|
|
369
|
+
| codex | cumulative `token_count` events in `~/.codex/sessions/…` |
|
|
370
|
+
| opencode, privacycode | the per-message `cost` each one computed itself |
|
|
371
|
+
| aider | the running session total it prints into `.aider.chat.history.md` |
|
|
372
|
+
|
|
373
|
+
**A `~` is an estimate, and an unmarked figure is not.** opencode and aider
|
|
374
|
+
price their own messages, and that price is reported untouched. Claude Code and
|
|
375
|
+
Codex record tokens only — which is the honest state of things on a
|
|
376
|
+
subscription, where the marginal request costs nothing extra — so those are
|
|
377
|
+
multiplied by published rates to answer "what would this have cost on the API".
|
|
378
|
+
|
|
379
|
+
A model nobody has priced shows its tokens and no cost, rather than a
|
|
380
|
+
convincing-looking zero. Price it yourself in `~/.moshcode/pricing.json`:
|
|
381
|
+
|
|
382
|
+
```json
|
|
383
|
+
{ "gpt-5.6-sol": { "input": 1.25, "output": 10 } }
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Cache tokens get their own column because on a long agent session they are most
|
|
387
|
+
of the traffic and a tenth of the price; folding them into `in` makes a $3
|
|
388
|
+
session look like a $60 one. Attribution is engine + directory + "started before
|
|
389
|
+
this run did", so a session that shares a directory with another agent can pick
|
|
390
|
+
up its neighbour's work — `--json` carries the run list when you need to check.
|
|
391
|
+
gemini, kimi, qwen, deepseek and openagents keep no readable usage log, so they
|
|
392
|
+
report no cost rather than zero cost.
|
|
393
|
+
|
|
341
394
|
### Driving it from a script or another agent
|
|
342
395
|
|
|
343
396
|
There is no second API — every verb takes `--json`, and that is what a machine
|
|
@@ -363,6 +416,120 @@ await herdWait("api"); await herdWait("web");
|
|
|
363
416
|
say(herdRead("api", { lines: 20 }));
|
|
364
417
|
```
|
|
365
418
|
|
|
419
|
+
Fanning work out is easy; joining on it used to be a hand-rolled polling loop.
|
|
420
|
+
`--any` returns on the first session to get there, `--all` when the last one
|
|
421
|
+
has, and both take the same `--state` and `--timeout` as a single wait:
|
|
422
|
+
|
|
423
|
+
```sh
|
|
424
|
+
moshcode wait --any api web docs # --json names the winner
|
|
425
|
+
moshcode wait --all api web --state done
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
const first = await herdWait(["api", "web", "docs"], { any: true });
|
|
430
|
+
await herdWait(["api", "web"], { states: ["done"] });
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Let the engine say what it is doing
|
|
434
|
+
|
|
435
|
+
Reading a screen works and it rots — engines change their wording between
|
|
436
|
+
releases and nothing tells you. When an engine has lifecycle hooks, install
|
|
437
|
+
them once and its state comes from the engine itself:
|
|
438
|
+
|
|
439
|
+
```sh
|
|
440
|
+
moshcode herd hooks install claude
|
|
441
|
+
✓ claude — 3 hooks installed (stop, notification, prompt-submit)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`moshcode ps` then reads `hook` in its last column instead of `screen`. The
|
|
445
|
+
file is **merged, never clobbered** — your own hooks stay, and `hooks remove`
|
|
446
|
+
takes out only what moshcode put in. A hook that fires outside a herd session
|
|
447
|
+
does nothing and exits 0, so installing one cannot break an engine you run by
|
|
448
|
+
hand, and the screen rules stay as the fallback for everything else.
|
|
449
|
+
`moshcode herd doctor` says what is installed, what has drifted, and — for the
|
|
450
|
+
first time — what is wrong with your `rules.json` instead of ignoring it.
|
|
451
|
+
|
|
452
|
+
### What happened while you slept
|
|
453
|
+
|
|
454
|
+
Every prompt through the herd mints a **task**: an id, its state transitions
|
|
455
|
+
with timestamps, and the output it produced. `ps` still answers "now"; this
|
|
456
|
+
answers "what happened".
|
|
457
|
+
|
|
458
|
+
```sh
|
|
459
|
+
$ moshcode herd tasks api
|
|
460
|
+
t-01 22:14 done 4m "port the auth routes"
|
|
461
|
+
t-02 22:19 blocked 6h11 "run the migration"
|
|
462
|
+
|
|
463
|
+
$ moshcode herd task t-02 # transitions, and what came back
|
|
464
|
+
$ moshcode herd log api # the raw state history
|
|
465
|
+
$ moshcode herd stats api
|
|
466
|
+
api working 3h02 · blocked 6h11 · idle 1h40
|
|
467
|
+
blocked 6h11 over 2 spell(s) — that one is you
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Blocked time is the herd's name for *human latency*: the agent was ready and
|
|
471
|
+
you were asleep. Ledgers live in `~/.moshcode/herd/tasks/<session>.jsonl` at
|
|
472
|
+
`0600`, capped at the last 500 tasks per session. From a script,
|
|
473
|
+
`herdTasks(name)` and `herdTask(id)` return them as values.
|
|
474
|
+
|
|
475
|
+
### Agents that are not on this box
|
|
476
|
+
|
|
477
|
+
A deployed agent can be a herd member. Two kinds: `a2a` speaks
|
|
478
|
+
[A2A v0.3.0](https://a2a-protocol.org/v0.3.0/specification/) (card discovery,
|
|
479
|
+
`message/send`, `tasks/get`, `tasks/cancel`), and `run` is a bare endpoint that
|
|
480
|
+
takes `POST {"prompt": …}` — the shape a `gradient agent deploy` prints.
|
|
481
|
+
|
|
482
|
+
```sh
|
|
483
|
+
moshcode herd remote add research https://agents.do-ai.run/…/production --kind run
|
|
484
|
+
export MOSHCODE_REMOTE_RESEARCH_TOKEN=… # never written to the manifest, never synced
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
$ moshcode ps
|
|
489
|
+
api claude blocked ~/src/coinpay 12m hook
|
|
490
|
+
research remote idle agents.do-ai.run — remote
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`prompt`, `read`, `wait` and `kill` work on it unchanged, which is the point: a
|
|
494
|
+
fan-out script across a local pty and a deployed agent contains no `if
|
|
495
|
+
(remote)`. A remote's state is the *remote's claim* — `ps` says `remote` in the
|
|
496
|
+
last column so it is never mistaken for something this box verified — and
|
|
497
|
+
`kill` on one deregisters it here rather than reaching across the network to
|
|
498
|
+
end somebody else's agent.
|
|
499
|
+
|
|
500
|
+
### The herd, over A2A
|
|
501
|
+
|
|
502
|
+
`moshcode herd serve` exposes this machine's herd to any A2A client: the herd's
|
|
503
|
+
card at `/.well-known/agent-card.json`, each member at `/<name>/`,
|
|
504
|
+
`message/send` → prompt, `tasks/get` → the ledger, `tasks/cancel` → interrupt.
|
|
505
|
+
`blocked` is A2A's `input-required`; the states that do not map cleanly round
|
|
506
|
+
down and carry the honest one in task metadata.
|
|
507
|
+
|
|
508
|
+
```sh
|
|
509
|
+
moshcode login # it verifies tokens against app.moshcode.sh
|
|
510
|
+
moshcode herd serve # 127.0.0.1:7683 by default
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
It is a shell on a socket and is treated like one: **no unauthenticated mode,
|
|
514
|
+
loopback included**, a loud warning past `127.0.0.1`, and sessions started with
|
|
515
|
+
`--agent` withheld unless you pass `--expose-autonomous` — an engine with its
|
|
516
|
+
approvals bypassed plus a network prompt is the worst pairing on the menu.
|
|
517
|
+
|
|
518
|
+
### Which engine is best at *this* repo
|
|
519
|
+
|
|
520
|
+
Not a leaderboard run against engines nobody deploys on repos nobody has — your
|
|
521
|
+
dataset, your engines, your machine:
|
|
522
|
+
|
|
523
|
+
```sh
|
|
524
|
+
moshcode herd eval --dataset evals/moshcode.jsonl --engines claude,codex --threshold 0.8
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
A row is `{"prompt": "…", "expect": "pattern"}` or
|
|
528
|
+
`{"prompt": "…", "rubric": "…"}` (jsonl, json or csv). Scoring is either the
|
|
529
|
+
dataset's own patterns or an engine acting as judge (`--judge claude`). Exit
|
|
530
|
+
codes are distinct on purpose — `0` pass, `4` below the threshold, `5` the
|
|
531
|
+
harness could not run — because CI has to tell a worse agent from a broken box.
|
|
532
|
+
|
|
366
533
|
### After a reboot
|
|
367
534
|
|
|
368
535
|
```sh
|
|
@@ -1013,6 +1180,9 @@ agents("claude"); // drop into an autonomous sessio
|
|
|
1013
1180
|
|
|
1014
1181
|
```sh
|
|
1015
1182
|
moshcode run examples/alive.mosh # run a script
|
|
1183
|
+
moshcode run examples/account.mosh --dry-run # log in, then do work that needs an account
|
|
1184
|
+
moshcode run examples/aliases.mosh --dry-run # define and run the pit's shortcuts
|
|
1185
|
+
moshcode run examples/research-desk.mosh # stocksRead/cryptoRead/newsRead → one digest
|
|
1016
1186
|
moshcode run deploy.mosh --dry-run # narrate without executing
|
|
1017
1187
|
moshcode run alive.mosh --max 5 # bound the while loop (default 3)
|
|
1018
1188
|
moshcode run deploy.mosh staging --fast # extra args reach the script as argv
|
|
@@ -1058,6 +1228,35 @@ chmod +x deploy.mosh
|
|
|
1058
1228
|
| `stop()` | end the loop (`alive = false`) |
|
|
1059
1229
|
| `repeat()` | back to the top of the loop |
|
|
1060
1230
|
|
|
1231
|
+
**Account verbs** (see [Authentication](#authentication)):
|
|
1232
|
+
|
|
1233
|
+
| verb | description |
|
|
1234
|
+
|---|---|
|
|
1235
|
+
| `await requireLogin()` | gate — verify, log in if needed, **throw** if it can't; returns the user |
|
|
1236
|
+
| `await login({ device, browser, force })` | authenticate; no-op when already signed in; returns `{ ok, email, already }` |
|
|
1237
|
+
| `await whoami()` | the account as a value: `{ status, verified, api, user: { id, email, name, credits } }` |
|
|
1238
|
+
| `logout()` | forget this machine's credentials |
|
|
1239
|
+
|
|
1240
|
+
**Alias verbs** — the pit's own shortcuts (`~/.moshcode/aliases.json`), readable and writable from a script:
|
|
1241
|
+
|
|
1242
|
+
| verb | description |
|
|
1243
|
+
|---|---|
|
|
1244
|
+
| `alias()` | every alias, as a `name → line` map |
|
|
1245
|
+
| `alias(name)` | one alias's line, or `null` |
|
|
1246
|
+
| `alias(name, line)` | define one; refuses names moshcode already owns |
|
|
1247
|
+
| `unalias(name)` | forget one |
|
|
1248
|
+
| `runAlias(name, …args)` | run one, args appended; returns `{ ok, code }` |
|
|
1249
|
+
|
|
1250
|
+
**Read verbs** — the tools as *values* rather than tables:
|
|
1251
|
+
|
|
1252
|
+
| verb | description |
|
|
1253
|
+
|---|---|
|
|
1254
|
+
| `await stocksRead(…)` | same args as `stocks(…)`, returns the parsed JSON |
|
|
1255
|
+
| `await cryptoRead(…)` | same args as `crypto(…)`, returns the parsed JSON |
|
|
1256
|
+
| `await newsRead({ list, limit })` | headlines as `[{ title, link, source, date }, …]` |
|
|
1257
|
+
| `herdRead(name, { lines })` | a herd session's screen, as a string |
|
|
1258
|
+
| `herdList()` | the roster: `[{ name, engine, state, cwd, alive }, …]` |
|
|
1259
|
+
|
|
1061
1260
|
**CLI verbs** (each shells out to `moshcode <name> ...args`):
|
|
1062
1261
|
|
|
1063
1262
|
| verb | description |
|
|
@@ -1082,7 +1281,26 @@ chmod +x deploy.mosh
|
|
|
1082
1281
|
| `turso(args…)` | drive the Turso CLI |
|
|
1083
1282
|
| `tailscale(args…)` | drive the Tailscale CLI |
|
|
1084
1283
|
| `coral(args…)` | drive the Coral CLI (SQL over APIs, databases, internal systems) |
|
|
1284
|
+
| `alpaca(args…)` | drive the native Alpaca trading CLI |
|
|
1085
1285
|
| `mcpjam(args…)` | drive the MCPJam CLI (test, debug, and validate MCP servers) |
|
|
1286
|
+
| `trade(args…)` | look up tickers, inspect markets, preview/place Alpaca orders |
|
|
1287
|
+
| `stocks(args…)` | research tickers via advis0r (`stocksRead` returns the data) |
|
|
1288
|
+
| `crypto(args…)` | research crypto pairs via advis0r (`cryptoRead` returns the data) |
|
|
1289
|
+
| `advisor(args…)` | query advis0r directly |
|
|
1290
|
+
| `news(args…)` | read, search, and subscribe to news feeds (`newsRead` returns the items) |
|
|
1291
|
+
| `rss(args…)` | manage RSS subscriptions and reading lists |
|
|
1292
|
+
| `plugin(args…)` | install/manage moshcode plugins from the marketplace |
|
|
1293
|
+
| `engines()` | list coding engines and whether they're installed |
|
|
1294
|
+
| `tools()` | list the adjacent workflow CLIs and whether they're installed |
|
|
1295
|
+
| `dns(args…)` | drive the Moshpit DNS bridge (enable, status, resolve) |
|
|
1296
|
+
| `doh(args…)` | run/inspect the DNS-over-HTTPS endpoint |
|
|
1297
|
+
| `site(args…)` | scaffold and publish a site |
|
|
1298
|
+
| `serve(args…)` | serve a directory over HTTP |
|
|
1299
|
+
| `template(args…)` | scaffold from a moshcode template |
|
|
1300
|
+
| `save()` / `load()` | push/pull settings to your moshcode account (needs login) |
|
|
1301
|
+
| `herd(args…)` | drive the herd (`herdStart`/`herdWait`/`herdRead` return values) |
|
|
1302
|
+
| `ps()` | print the herd roster |
|
|
1303
|
+
| `ai(prompt, { engine })` | run an engine headlessly and **return** its output as a string |
|
|
1086
1304
|
| `pwd()` | print the current repo/location |
|
|
1087
1305
|
| `run(file)` | run another .mosh file (include/compose) |
|
|
1088
1306
|
|
|
@@ -1094,6 +1312,81 @@ chmod +x deploy.mosh
|
|
|
1094
1312
|
| `argv` | positional args passed after the script file |
|
|
1095
1313
|
| `env` | `process.env` — parameterize scripts from the environment |
|
|
1096
1314
|
|
|
1315
|
+
### Authentication
|
|
1316
|
+
|
|
1317
|
+
Some verbs need an account: `notify()`/`ask()` reach you through
|
|
1318
|
+
`app.moshcode.sh`, and `save()`/`load()` sync settings to it. A script says so
|
|
1319
|
+
once, at the top, instead of failing one call at a time later on:
|
|
1320
|
+
|
|
1321
|
+
```js
|
|
1322
|
+
const me = await requireLogin(); // verifies; logs in if it has to
|
|
1323
|
+
say(`signed in as ${me.email} (${me.credits} credits)`);
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
- **`requireLogin({ device, browser })`** — the gate. Verifies this machine
|
|
1327
|
+
against the app; if there's no usable session it runs the login flow, then
|
|
1328
|
+
re-checks. Returns the verified `{ id, email, name, credits }`. **Throws** if
|
|
1329
|
+
it still can't authenticate — the one verb here that does, because "require"
|
|
1330
|
+
means the script must not continue without an account.
|
|
1331
|
+
- **`login({ device, browser, force })`** — idempotent. Returns early with
|
|
1332
|
+
`{ already: true }` when you're already signed in, so a script you re-run all
|
|
1333
|
+
day never throws a browser tab at you. Returns `{ ok: false, error }` on
|
|
1334
|
+
failure rather than throwing, so a script can fall back to read-only work.
|
|
1335
|
+
- **`whoami()`** — the account as a value, verified against the app:
|
|
1336
|
+
`{ status, verified, api, user }` where `status` is `authenticated`,
|
|
1337
|
+
`not_logged_in`, `expired`, `unverified`, or `unreachable`. Never throws — an
|
|
1338
|
+
unreachable app is a status, not an exception.
|
|
1339
|
+
- **`logout()`** — forget the local credentials.
|
|
1340
|
+
|
|
1341
|
+
The flow is picked for where the script is running: the loopback/browser flow
|
|
1342
|
+
locally, and the device-code flow over SSH or on a headless box (where a
|
|
1343
|
+
`127.0.0.1` callback would land on the *browser's* machine and never arrive).
|
|
1344
|
+
`{ device: true }` / `{ browser: true }` pin it either way. Credentials live in
|
|
1345
|
+
`~/.moshcode/credentials.json` (mode `0600`) — the same ones `moshcode login`
|
|
1346
|
+
writes, so logging in once covers the CLI, the pit, and every script.
|
|
1347
|
+
|
|
1348
|
+
```js
|
|
1349
|
+
// gate on the balance, not just the session
|
|
1350
|
+
const me = await whoami();
|
|
1351
|
+
if (!me.verified) { say("read-only run — not signed in"); }
|
|
1352
|
+
else if (me.user.credits < 10) notify(`only ${me.user.credits} credits left`);
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
### Aliases
|
|
1356
|
+
|
|
1357
|
+
The pit keeps named shortcuts for the lines you retype (`/alias set gs "git
|
|
1358
|
+
status"`). Scripts read and write the same store, so your vocabulary and
|
|
1359
|
+
moshcode's are one thing rather than two:
|
|
1360
|
+
|
|
1361
|
+
```js
|
|
1362
|
+
alias("gs", "git status --short"); // define (refuses names moshcode owns)
|
|
1363
|
+
alias("cc", "/agents claude"); // a leading `/` is a moshcode command
|
|
1364
|
+
runAlias("gs", "--branch"); // → git status --short --branch
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
Expansion is the pit's rule: a leading `/` routes to the moshcode command of
|
|
1368
|
+
that name, anything else is a shell line, and arguments are **appended** rather
|
|
1369
|
+
than substituted — exactly how a shell alias behaves. `runAlias()` returns
|
|
1370
|
+
`{ ok, code }` like `shell()`, and `{ ok: false, code: 127 }` when there's no
|
|
1371
|
+
such alias.
|
|
1372
|
+
|
|
1373
|
+
### Reading the tools, not just running them
|
|
1374
|
+
|
|
1375
|
+
`stocks report NVDA` prints a table; a script usually wants the number. The
|
|
1376
|
+
`*Read()` verbs call the same layer the printed commands render from and hand
|
|
1377
|
+
back parsed data:
|
|
1378
|
+
|
|
1379
|
+
```js
|
|
1380
|
+
const report = await stocksRead("report", "NVDA"); // → JSON, or null on error
|
|
1381
|
+
const btc = await cryptoRead("quote", "BTC/USD");
|
|
1382
|
+
const items = await newsRead({ limit: 5 }); // [{ title, link, source, date }]
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
A failed lookup returns `null` (or `[]`) rather than throwing, so one bad symbol
|
|
1386
|
+
doesn't take a briefing script down. Same reasoning as the herd's
|
|
1387
|
+
`herdRead()`/`herdList()`: shelling out gives you `{ ok, code }`, and the whole
|
|
1388
|
+
point of these is the value.
|
|
1389
|
+
|
|
1097
1390
|
### Human-in-the-loop
|
|
1098
1391
|
|
|
1099
1392
|
- `notify(msg)` — fire-and-forget. Pings the operator across configured channels
|
package/bin/moshcode.mjs
CHANGED
|
@@ -336,7 +336,7 @@ async function main() {
|
|
|
336
336
|
const [key, engine] = resolved;
|
|
337
337
|
return launchEngine(key, engine, rest.slice(1));
|
|
338
338
|
}
|
|
339
|
-
// The herd (PRD 0009). `herd` is the namespace; the
|
|
339
|
+
// The herd (PRD 0009). `herd` is the namespace; the verbs people reach
|
|
340
340
|
// for most often are also top-level, because `moshcode ps` is what someone
|
|
341
341
|
// types when they want to know what is running and nobody should have to
|
|
342
342
|
// learn a namespace to ask that.
|
|
@@ -344,8 +344,8 @@ async function main() {
|
|
|
344
344
|
process.exitCode = (await herdCommand(rest)) || 0;
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
|
-
if (["ps", "attach", "kill", "wait", "restore"].includes(cmd)) {
|
|
348
|
-
process.exitCode = (await herdCommand([cmd === "
|
|
347
|
+
if (["ps", "attach", "kill", "wait", "restore", "cost", "usage"].includes(cmd)) {
|
|
348
|
+
process.exitCode = (await herdCommand([cmd === "usage" ? "cost" : cmd, ...rest])) || 0;
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
if (cmd === "tools") {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env moshscript
|
|
2
|
+
// account.mosh — authenticate the operator, then do work that needs an account.
|
|
3
|
+
//
|
|
4
|
+
// `requireLogin()` is the gate: it verifies this machine against
|
|
5
|
+
// app.moshcode.sh and, if there is no usable session, runs the login flow
|
|
6
|
+
// (browser locally, device code over SSH) before the script continues. Anything
|
|
7
|
+
// downstream that needs an account — notify(), ask(), save()/load() — would
|
|
8
|
+
// otherwise fail one call at a time, much later and much less legibly.
|
|
9
|
+
//
|
|
10
|
+
// Try it safely first:
|
|
11
|
+
//
|
|
12
|
+
// moshcode run examples/account.mosh --dry-run
|
|
13
|
+
// chmod +x examples/account.mosh && ./examples/account.mosh 5
|
|
14
|
+
//
|
|
15
|
+
// argv[0] is the minimum credit balance to demand (default: 1).
|
|
16
|
+
|
|
17
|
+
const needCredits = Number(argv[0] || 1);
|
|
18
|
+
|
|
19
|
+
// Blocks until there is a verified account. THROWS if it can't get one — that
|
|
20
|
+
// is what "require" means, and it stops the script here rather than halfway
|
|
21
|
+
// through the work.
|
|
22
|
+
const me = await requireLogin();
|
|
23
|
+
say(`🤘 signed in as ${me.email || me.name || "moshcoder"}`);
|
|
24
|
+
|
|
25
|
+
// whoami() hands the account back as a value, so a script can branch on it
|
|
26
|
+
// instead of re-parsing `moshcode whoami` output.
|
|
27
|
+
if (me.credits != null && me.credits < needCredits) {
|
|
28
|
+
say(`⛔ ${me.credits} credits left, need ${needCredits} — topping up is a human job`);
|
|
29
|
+
notify(`moshcode is out of credits (${me.credits}) — top up to keep the pit going`);
|
|
30
|
+
stop();
|
|
31
|
+
} else {
|
|
32
|
+
// Settings sync needs the account we just proved we have.
|
|
33
|
+
load(); // → moshcode load (pull settings down)
|
|
34
|
+
say("⚙️ settings pulled from your account");
|
|
35
|
+
|
|
36
|
+
const test = shell("pnpm -r test");
|
|
37
|
+
if (!test.ok) {
|
|
38
|
+
// The human-in-the-loop gate: this blocks until someone answers at
|
|
39
|
+
// app.moshcode.sh/approve/:id — which only works because we're logged in.
|
|
40
|
+
const next = await ask(`tests failed (exit ${test.code}) — ship anyway, or fix?`);
|
|
41
|
+
say(`👤 operator says: ${next ?? "(no reply — stopping)"}`);
|
|
42
|
+
if (next == null) stop();
|
|
43
|
+
} else {
|
|
44
|
+
say("✅ green — saving settings back up");
|
|
45
|
+
save(); // → moshcode save (push settings up)
|
|
46
|
+
notify("moshcode: tests green, settings synced 🤘");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env moshscript
|
|
2
|
+
// aliases.mosh — the pit's shortcuts, from a script.
|
|
3
|
+
//
|
|
4
|
+
// The pit keeps named shortcuts for the lines you retype (`/alias set gs "git
|
|
5
|
+
// status"`). They are your vocabulary, not moshcode's, so a script can read the
|
|
6
|
+
// same store, define new ones, and run them — instead of re-spelling every one
|
|
7
|
+
// of those lines in every script.
|
|
8
|
+
//
|
|
9
|
+
// The expansion rule is the pit's: a leading `/` is a moshcode command,
|
|
10
|
+
// anything else is a shell line, and arguments are appended rather than
|
|
11
|
+
// substituted — so runAlias("gs", "--short") is `git status --short`.
|
|
12
|
+
//
|
|
13
|
+
// Try it safely first:
|
|
14
|
+
//
|
|
15
|
+
// moshcode run examples/aliases.mosh --dry-run
|
|
16
|
+
// chmod +x examples/aliases.mosh && ./examples/aliases.mosh
|
|
17
|
+
//
|
|
18
|
+
// argv[0] is the engine an alias should open (default: claude).
|
|
19
|
+
|
|
20
|
+
const engine = argv[0] || "claude";
|
|
21
|
+
|
|
22
|
+
// Set up a small kit. alias(name, line) returns { ok, error, previous }; a name
|
|
23
|
+
// moshcode already owns (`agents`, `install`, …) is refused rather than
|
|
24
|
+
// shadowed, because a shortcut that silently does nothing is worse than one
|
|
25
|
+
// that was never accepted.
|
|
26
|
+
const kit = {
|
|
27
|
+
gs: "git status --short --branch",
|
|
28
|
+
gl: "git log --oneline -12",
|
|
29
|
+
cc: `/agents ${engine}`, // a leading `/` → the moshcode command
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (const [name, line] of Object.entries(kit)) {
|
|
33
|
+
const r = alias(name, line);
|
|
34
|
+
if (!r.ok) say(`⚠️ couldn't define /${name} — ${r.error}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// alias() with no arguments is the whole map — the same one the pit shows.
|
|
38
|
+
const all = alias();
|
|
39
|
+
say(`🔖 ${Object.keys(all).length} alias(es) on this machine`);
|
|
40
|
+
|
|
41
|
+
// Run one. Returns { ok, code } like shell() and the CLI verbs, so a script can
|
|
42
|
+
// branch on the outcome without try/catch.
|
|
43
|
+
//
|
|
44
|
+
// Under --dry-run nothing above was actually written, so these report the
|
|
45
|
+
// aliases as missing (code 127) rather than pretending to run them — a dry run
|
|
46
|
+
// narrates what it would do, and it genuinely doesn't know what an undefined
|
|
47
|
+
// alias expands to. Run it for real to see them execute.
|
|
48
|
+
const status = runAlias("gs");
|
|
49
|
+
if (!status.ok && status.code !== 127) say(`git status exited ${status.code}`);
|
|
50
|
+
|
|
51
|
+
runAlias("gl", "--author", env.USER || ""); // extra args are appended
|
|
52
|
+
|
|
53
|
+
// Clean up the throwaway ones, leave the useful one behind.
|
|
54
|
+
unalias("gl");
|
|
55
|
+
|
|
56
|
+
say(`🤘 /cc is yours now — type it in the pit to open ${engine}`);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env moshscript
|
|
2
|
+
// research-desk.mosh — a morning briefing, assembled from the read verbs.
|
|
3
|
+
//
|
|
4
|
+
// `stocks report NVDA` prints a table; `stocksRead("report", "NVDA")` hands
|
|
5
|
+
// back the JSON. That is the difference this script is built on: the *Read()
|
|
6
|
+
// verbs return values, so a script can rank, threshold, and summarize instead
|
|
7
|
+
// of scraping stdout. Same advis0r/feed layer the printed commands render from.
|
|
8
|
+
//
|
|
9
|
+
// Try it safely first:
|
|
10
|
+
//
|
|
11
|
+
// moshcode run examples/research-desk.mosh --dry-run
|
|
12
|
+
// chmod +x examples/research-desk.mosh && ./examples/research-desk.mosh NVDA AMD
|
|
13
|
+
//
|
|
14
|
+
// argv is the ticker list (default: NVDA TSLA).
|
|
15
|
+
|
|
16
|
+
const tickers = argv.length ? argv : ["NVDA", "TSLA"];
|
|
17
|
+
const briefing = [];
|
|
18
|
+
|
|
19
|
+
// Rank the tickers by whatever score advis0r returns, keeping only the ones
|
|
20
|
+
// worth a human's attention. A failed lookup comes back null rather than
|
|
21
|
+
// throwing, so one bad symbol doesn't take the briefing down.
|
|
22
|
+
for (const symbol of tickers) {
|
|
23
|
+
const data = await stocksRead("report", symbol);
|
|
24
|
+
if (!data) { say(`⚠️ no data for ${symbol}`); continue; }
|
|
25
|
+
const score = data.score ?? data.report?.score ?? null;
|
|
26
|
+
briefing.push({ symbol, score });
|
|
27
|
+
say(`📈 ${symbol}: ${score ?? "no score"}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Crypto reads the same way.
|
|
31
|
+
const btc = await cryptoRead("quote", "BTC/USD");
|
|
32
|
+
if (btc) say(`🪙 BTC/USD: ${btc.price ?? btc.quote?.price ?? "?"}`);
|
|
33
|
+
|
|
34
|
+
// Headlines from your own subscriptions (or pass { list: "smallweb" } for one
|
|
35
|
+
// of the built-in feed lists).
|
|
36
|
+
const headlines = await newsRead({ limit: 5 });
|
|
37
|
+
for (const item of headlines) {
|
|
38
|
+
say(`📰 ${item.source ? `[${item.source}] ` : ""}${item.title}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// One ping with the whole picture, rather than one per lookup.
|
|
42
|
+
const movers = briefing
|
|
43
|
+
.filter((b) => b.score != null)
|
|
44
|
+
.sort((a, b) => b.score - a.score)
|
|
45
|
+
.slice(0, 3)
|
|
46
|
+
.map((b) => `${b.symbol} ${b.score}`)
|
|
47
|
+
.join(", ");
|
|
48
|
+
|
|
49
|
+
notify(`morning desk — ${movers || "no scores today"} · ${headlines.length} headline(s)`);
|
package/package.json
CHANGED