moshcode 0.57.0 → 0.59.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 +179 -0
- package/bin/moshcode.mjs +2 -2
- 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/src/auth.mjs +59 -64
- package/src/cli-schema.mjs +35 -0
- package/src/commands.mjs +305 -29
- package/src/cost-cli.mjs +232 -0
- package/src/cost-pricing.mjs +159 -0
- package/src/cost.mjs +634 -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 +4 -0
- package/src/tui.mjs +38 -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` | 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 |
|
|
@@ -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
|
|
@@ -1013,6 +1066,9 @@ agents("claude"); // drop into an autonomous sessio
|
|
|
1013
1066
|
|
|
1014
1067
|
```sh
|
|
1015
1068
|
moshcode run examples/alive.mosh # run a script
|
|
1069
|
+
moshcode run examples/account.mosh --dry-run # log in, then do work that needs an account
|
|
1070
|
+
moshcode run examples/aliases.mosh --dry-run # define and run the pit's shortcuts
|
|
1071
|
+
moshcode run examples/research-desk.mosh # stocksRead/cryptoRead/newsRead → one digest
|
|
1016
1072
|
moshcode run deploy.mosh --dry-run # narrate without executing
|
|
1017
1073
|
moshcode run alive.mosh --max 5 # bound the while loop (default 3)
|
|
1018
1074
|
moshcode run deploy.mosh staging --fast # extra args reach the script as argv
|
|
@@ -1058,6 +1114,35 @@ chmod +x deploy.mosh
|
|
|
1058
1114
|
| `stop()` | end the loop (`alive = false`) |
|
|
1059
1115
|
| `repeat()` | back to the top of the loop |
|
|
1060
1116
|
|
|
1117
|
+
**Account verbs** (see [Authentication](#authentication)):
|
|
1118
|
+
|
|
1119
|
+
| verb | description |
|
|
1120
|
+
|---|---|
|
|
1121
|
+
| `await requireLogin()` | gate — verify, log in if needed, **throw** if it can't; returns the user |
|
|
1122
|
+
| `await login({ device, browser, force })` | authenticate; no-op when already signed in; returns `{ ok, email, already }` |
|
|
1123
|
+
| `await whoami()` | the account as a value: `{ status, verified, api, user: { id, email, name, credits } }` |
|
|
1124
|
+
| `logout()` | forget this machine's credentials |
|
|
1125
|
+
|
|
1126
|
+
**Alias verbs** — the pit's own shortcuts (`~/.moshcode/aliases.json`), readable and writable from a script:
|
|
1127
|
+
|
|
1128
|
+
| verb | description |
|
|
1129
|
+
|---|---|
|
|
1130
|
+
| `alias()` | every alias, as a `name → line` map |
|
|
1131
|
+
| `alias(name)` | one alias's line, or `null` |
|
|
1132
|
+
| `alias(name, line)` | define one; refuses names moshcode already owns |
|
|
1133
|
+
| `unalias(name)` | forget one |
|
|
1134
|
+
| `runAlias(name, …args)` | run one, args appended; returns `{ ok, code }` |
|
|
1135
|
+
|
|
1136
|
+
**Read verbs** — the tools as *values* rather than tables:
|
|
1137
|
+
|
|
1138
|
+
| verb | description |
|
|
1139
|
+
|---|---|
|
|
1140
|
+
| `await stocksRead(…)` | same args as `stocks(…)`, returns the parsed JSON |
|
|
1141
|
+
| `await cryptoRead(…)` | same args as `crypto(…)`, returns the parsed JSON |
|
|
1142
|
+
| `await newsRead({ list, limit })` | headlines as `[{ title, link, source, date }, …]` |
|
|
1143
|
+
| `herdRead(name, { lines })` | a herd session's screen, as a string |
|
|
1144
|
+
| `herdList()` | the roster: `[{ name, engine, state, cwd, alive }, …]` |
|
|
1145
|
+
|
|
1061
1146
|
**CLI verbs** (each shells out to `moshcode <name> ...args`):
|
|
1062
1147
|
|
|
1063
1148
|
| verb | description |
|
|
@@ -1082,7 +1167,26 @@ chmod +x deploy.mosh
|
|
|
1082
1167
|
| `turso(args…)` | drive the Turso CLI |
|
|
1083
1168
|
| `tailscale(args…)` | drive the Tailscale CLI |
|
|
1084
1169
|
| `coral(args…)` | drive the Coral CLI (SQL over APIs, databases, internal systems) |
|
|
1170
|
+
| `alpaca(args…)` | drive the native Alpaca trading CLI |
|
|
1085
1171
|
| `mcpjam(args…)` | drive the MCPJam CLI (test, debug, and validate MCP servers) |
|
|
1172
|
+
| `trade(args…)` | look up tickers, inspect markets, preview/place Alpaca orders |
|
|
1173
|
+
| `stocks(args…)` | research tickers via advis0r (`stocksRead` returns the data) |
|
|
1174
|
+
| `crypto(args…)` | research crypto pairs via advis0r (`cryptoRead` returns the data) |
|
|
1175
|
+
| `advisor(args…)` | query advis0r directly |
|
|
1176
|
+
| `news(args…)` | read, search, and subscribe to news feeds (`newsRead` returns the items) |
|
|
1177
|
+
| `rss(args…)` | manage RSS subscriptions and reading lists |
|
|
1178
|
+
| `plugin(args…)` | install/manage moshcode plugins from the marketplace |
|
|
1179
|
+
| `engines()` | list coding engines and whether they're installed |
|
|
1180
|
+
| `tools()` | list the adjacent workflow CLIs and whether they're installed |
|
|
1181
|
+
| `dns(args…)` | drive the Moshpit DNS bridge (enable, status, resolve) |
|
|
1182
|
+
| `doh(args…)` | run/inspect the DNS-over-HTTPS endpoint |
|
|
1183
|
+
| `site(args…)` | scaffold and publish a site |
|
|
1184
|
+
| `serve(args…)` | serve a directory over HTTP |
|
|
1185
|
+
| `template(args…)` | scaffold from a moshcode template |
|
|
1186
|
+
| `save()` / `load()` | push/pull settings to your moshcode account (needs login) |
|
|
1187
|
+
| `herd(args…)` | drive the herd (`herdStart`/`herdWait`/`herdRead` return values) |
|
|
1188
|
+
| `ps()` | print the herd roster |
|
|
1189
|
+
| `ai(prompt, { engine })` | run an engine headlessly and **return** its output as a string |
|
|
1086
1190
|
| `pwd()` | print the current repo/location |
|
|
1087
1191
|
| `run(file)` | run another .mosh file (include/compose) |
|
|
1088
1192
|
|
|
@@ -1094,6 +1198,81 @@ chmod +x deploy.mosh
|
|
|
1094
1198
|
| `argv` | positional args passed after the script file |
|
|
1095
1199
|
| `env` | `process.env` — parameterize scripts from the environment |
|
|
1096
1200
|
|
|
1201
|
+
### Authentication
|
|
1202
|
+
|
|
1203
|
+
Some verbs need an account: `notify()`/`ask()` reach you through
|
|
1204
|
+
`app.moshcode.sh`, and `save()`/`load()` sync settings to it. A script says so
|
|
1205
|
+
once, at the top, instead of failing one call at a time later on:
|
|
1206
|
+
|
|
1207
|
+
```js
|
|
1208
|
+
const me = await requireLogin(); // verifies; logs in if it has to
|
|
1209
|
+
say(`signed in as ${me.email} (${me.credits} credits)`);
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
- **`requireLogin({ device, browser })`** — the gate. Verifies this machine
|
|
1213
|
+
against the app; if there's no usable session it runs the login flow, then
|
|
1214
|
+
re-checks. Returns the verified `{ id, email, name, credits }`. **Throws** if
|
|
1215
|
+
it still can't authenticate — the one verb here that does, because "require"
|
|
1216
|
+
means the script must not continue without an account.
|
|
1217
|
+
- **`login({ device, browser, force })`** — idempotent. Returns early with
|
|
1218
|
+
`{ already: true }` when you're already signed in, so a script you re-run all
|
|
1219
|
+
day never throws a browser tab at you. Returns `{ ok: false, error }` on
|
|
1220
|
+
failure rather than throwing, so a script can fall back to read-only work.
|
|
1221
|
+
- **`whoami()`** — the account as a value, verified against the app:
|
|
1222
|
+
`{ status, verified, api, user }` where `status` is `authenticated`,
|
|
1223
|
+
`not_logged_in`, `expired`, `unverified`, or `unreachable`. Never throws — an
|
|
1224
|
+
unreachable app is a status, not an exception.
|
|
1225
|
+
- **`logout()`** — forget the local credentials.
|
|
1226
|
+
|
|
1227
|
+
The flow is picked for where the script is running: the loopback/browser flow
|
|
1228
|
+
locally, and the device-code flow over SSH or on a headless box (where a
|
|
1229
|
+
`127.0.0.1` callback would land on the *browser's* machine and never arrive).
|
|
1230
|
+
`{ device: true }` / `{ browser: true }` pin it either way. Credentials live in
|
|
1231
|
+
`~/.moshcode/credentials.json` (mode `0600`) — the same ones `moshcode login`
|
|
1232
|
+
writes, so logging in once covers the CLI, the pit, and every script.
|
|
1233
|
+
|
|
1234
|
+
```js
|
|
1235
|
+
// gate on the balance, not just the session
|
|
1236
|
+
const me = await whoami();
|
|
1237
|
+
if (!me.verified) { say("read-only run — not signed in"); }
|
|
1238
|
+
else if (me.user.credits < 10) notify(`only ${me.user.credits} credits left`);
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
### Aliases
|
|
1242
|
+
|
|
1243
|
+
The pit keeps named shortcuts for the lines you retype (`/alias set gs "git
|
|
1244
|
+
status"`). Scripts read and write the same store, so your vocabulary and
|
|
1245
|
+
moshcode's are one thing rather than two:
|
|
1246
|
+
|
|
1247
|
+
```js
|
|
1248
|
+
alias("gs", "git status --short"); // define (refuses names moshcode owns)
|
|
1249
|
+
alias("cc", "/agents claude"); // a leading `/` is a moshcode command
|
|
1250
|
+
runAlias("gs", "--branch"); // → git status --short --branch
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
Expansion is the pit's rule: a leading `/` routes to the moshcode command of
|
|
1254
|
+
that name, anything else is a shell line, and arguments are **appended** rather
|
|
1255
|
+
than substituted — exactly how a shell alias behaves. `runAlias()` returns
|
|
1256
|
+
`{ ok, code }` like `shell()`, and `{ ok: false, code: 127 }` when there's no
|
|
1257
|
+
such alias.
|
|
1258
|
+
|
|
1259
|
+
### Reading the tools, not just running them
|
|
1260
|
+
|
|
1261
|
+
`stocks report NVDA` prints a table; a script usually wants the number. The
|
|
1262
|
+
`*Read()` verbs call the same layer the printed commands render from and hand
|
|
1263
|
+
back parsed data:
|
|
1264
|
+
|
|
1265
|
+
```js
|
|
1266
|
+
const report = await stocksRead("report", "NVDA"); // → JSON, or null on error
|
|
1267
|
+
const btc = await cryptoRead("quote", "BTC/USD");
|
|
1268
|
+
const items = await newsRead({ limit: 5 }); // [{ title, link, source, date }]
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
A failed lookup returns `null` (or `[]`) rather than throwing, so one bad symbol
|
|
1272
|
+
doesn't take a briefing script down. Same reasoning as the herd's
|
|
1273
|
+
`herdRead()`/`herdList()`: shelling out gives you `{ ok, code }`, and the whole
|
|
1274
|
+
point of these is the value.
|
|
1275
|
+
|
|
1097
1276
|
### Human-in-the-loop
|
|
1098
1277
|
|
|
1099
1278
|
- `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,7 +344,7 @@ 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)) {
|
|
347
|
+
if (["ps", "attach", "kill", "wait", "restore", "cost"].includes(cmd)) {
|
|
348
348
|
process.exitCode = (await herdCommand([cmd === "ps" ? "ps" : cmd, ...rest])) || 0;
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
@@ -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
package/src/auth.mjs
CHANGED
|
@@ -166,89 +166,84 @@ export async function loginAuto({ device = false, browser = false } = {}) {
|
|
|
166
166
|
return loginDevice({ open: !isRemoteShell() });
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Who is logged in, as a value — the shape `whoami --json` prints, and what
|
|
171
|
+
* moshscript's whoami()/requireLogin() read (src/commands.mjs).
|
|
172
|
+
*
|
|
173
|
+
* Returning rather than printing is the point: a script needs to branch on the
|
|
174
|
+
* account ("do I have credits?", "is this the right email?"), and the only way
|
|
175
|
+
* to get that out of a printing function is to re-parse its stdout. One
|
|
176
|
+
* verified-identity implementation, two callers — the CLI renders it, the
|
|
177
|
+
* script reads it — so the two can never disagree about what "logged in" means.
|
|
178
|
+
*
|
|
179
|
+
* Never throws: an unreachable app is a `status` like any other, because the
|
|
180
|
+
* caller is usually deciding whether to *start* work, not reporting an outage.
|
|
181
|
+
*/
|
|
182
|
+
export async function identity() {
|
|
171
183
|
const creds = loadCreds();
|
|
172
184
|
if (!creds?.token) {
|
|
173
|
-
|
|
174
|
-
console.log(JSON.stringify({
|
|
175
|
-
status: "not_logged_in",
|
|
176
|
-
verified: false,
|
|
177
|
-
api: API(),
|
|
178
|
-
user: null,
|
|
179
|
-
}, null, 2));
|
|
180
|
-
} else {
|
|
181
|
-
console.log("not logged in — run: moshcode login");
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
185
|
+
return { status: "not_logged_in", verified: false, api: API(), user: null };
|
|
184
186
|
}
|
|
185
187
|
const api = creds.api || API();
|
|
188
|
+
// What we know locally, used for every unverified outcome. Deliberately not
|
|
189
|
+
// presented as confirmed: the app is the authority on the account, and these
|
|
190
|
+
// fields are only what the last successful login happened to write down.
|
|
186
191
|
const localUser = {
|
|
187
192
|
id: creds.id ?? null,
|
|
188
193
|
email: creds.email ?? null,
|
|
189
194
|
name: null,
|
|
190
195
|
credits: null,
|
|
191
196
|
};
|
|
192
|
-
const printJson = (value) => console.log(JSON.stringify(value, null, 2));
|
|
193
197
|
try {
|
|
194
198
|
const res = await fetch(`${api}/api/me`, { headers: { authorization: `Bearer ${creds.token}` } });
|
|
195
199
|
if (res.status === 401) {
|
|
196
|
-
|
|
197
|
-
printJson({
|
|
198
|
-
status: "expired",
|
|
199
|
-
verified: false,
|
|
200
|
-
api,
|
|
201
|
-
user: localUser,
|
|
202
|
-
error: { type: "auth", status: 401 },
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
else console.log("session expired — run: moshcode login");
|
|
206
|
-
return;
|
|
200
|
+
return { status: "expired", verified: false, api, user: localUser, error: { type: "auth", status: 401 } };
|
|
207
201
|
}
|
|
208
202
|
// Any other error status still has a body, and it isn't an account — reading
|
|
209
|
-
// it as one
|
|
203
|
+
// it as one reports a made-up identity for a session the app just refused.
|
|
210
204
|
if (!res.ok) {
|
|
211
|
-
|
|
212
|
-
printJson({
|
|
213
|
-
status: "unverified",
|
|
214
|
-
verified: false,
|
|
215
|
-
api,
|
|
216
|
-
user: localUser,
|
|
217
|
-
error: { type: "http", status: res.status },
|
|
218
|
-
});
|
|
219
|
-
} else {
|
|
220
|
-
console.log(`${creds.email || "logged in"} @ ${api} (couldn't verify — the app returned ${res.status})`);
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
205
|
+
return { status: "unverified", verified: false, api, user: localUser, error: { type: "http", status: res.status } };
|
|
223
206
|
}
|
|
224
207
|
const me = await res.json();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
237
|
-
} else {
|
|
238
|
-
console.log(`${me.email || me.name || "moshcoder"} 🤘 (${me.credits ?? "?"} credits) @ ${api}`);
|
|
239
|
-
}
|
|
208
|
+
return {
|
|
209
|
+
status: "authenticated",
|
|
210
|
+
verified: true,
|
|
211
|
+
api,
|
|
212
|
+
user: {
|
|
213
|
+
id: me.id ?? creds.id ?? null,
|
|
214
|
+
email: me.email ?? creds.email ?? null,
|
|
215
|
+
name: me.name ?? null,
|
|
216
|
+
credits: me.credits ?? null,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
240
219
|
} catch {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
220
|
+
return { status: "unreachable", verified: false, api, user: localUser, error: { type: "network" } };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Print who is logged in (verified against the app). */
|
|
225
|
+
export async function whoami({ json = false } = {}) {
|
|
226
|
+
const me = await identity();
|
|
227
|
+
if (json) {
|
|
228
|
+
console.log(JSON.stringify(me, null, 2));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const who = me.user?.email || me.user?.name;
|
|
232
|
+
switch (me.status) {
|
|
233
|
+
case "not_logged_in":
|
|
234
|
+
console.log("not logged in — run: moshcode login");
|
|
235
|
+
return;
|
|
236
|
+
case "expired":
|
|
237
|
+
console.log("session expired — run: moshcode login");
|
|
238
|
+
return;
|
|
239
|
+
case "unverified":
|
|
240
|
+
console.log(`${who || "logged in"} @ ${me.api} (couldn't verify — the app returned ${me.error.status})`);
|
|
241
|
+
return;
|
|
242
|
+
case "unreachable":
|
|
243
|
+
console.log(`${who || "logged in"} @ ${me.api} (couldn't reach the app to verify)`);
|
|
244
|
+
return;
|
|
245
|
+
default:
|
|
246
|
+
console.log(`${who || "moshcoder"} 🤘 (${me.user.credits ?? "?"} credits) @ ${me.api}`);
|
|
252
247
|
}
|
|
253
248
|
}
|
|
254
249
|
|
package/src/cli-schema.mjs
CHANGED
|
@@ -112,6 +112,31 @@ export const CORE_CLI_COMMANDS = [
|
|
|
112
112
|
seeAlso: ["herd", "attach", "wait"],
|
|
113
113
|
note: "state is idle, working, blocked, done or unknown — unknown is a safe answer, not a failure.",
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
name: "cost",
|
|
117
|
+
group: "runtime",
|
|
118
|
+
description: "what each session is spending, read from the engines' own logs",
|
|
119
|
+
synopsis: [["moshcode cost [name] [--all] [--since 6h] [--json]", "session, engine, model, tokens, cost"]],
|
|
120
|
+
flags: [
|
|
121
|
+
["--all", "every engine session on disk, herd or not", ""],
|
|
122
|
+
["--since <dur>", "how far back to look (30m, 6h, 3d)", "24h"],
|
|
123
|
+
["--engine <list>", "only these engines, comma-separated", "all"],
|
|
124
|
+
["--watch [secs]", "re-read on an interval until ctrl-c", "10"],
|
|
125
|
+
["--json", "machine-readable", ""],
|
|
126
|
+
],
|
|
127
|
+
examples: [
|
|
128
|
+
["moshcode cost", "what is the herd costing me right now?"],
|
|
129
|
+
["moshcode cost api --json", "one session, with its engine runs broken out"],
|
|
130
|
+
["moshcode cost --all --since 7d", "everything the agents did this week"],
|
|
131
|
+
],
|
|
132
|
+
seeAlso: ["ps", "herd", "attach"],
|
|
133
|
+
note: "the numbers come from each CLI's own session log — claude's ~/.claude/projects transcripts, "
|
|
134
|
+
+ "codex's rollout token counts, opencode's per-message cost, aider's chat history. a figure marked "
|
|
135
|
+
+ "`~` was worked out from published rates and is what the tokens WOULD cost on the api; unmarked "
|
|
136
|
+
+ "figures are the engine's own arithmetic. models with no rate show tokens and no cost — add yours "
|
|
137
|
+
+ "to ~/.moshcode/pricing.json. gemini, kimi, qwen, deepseek and openagents log nothing readable, "
|
|
138
|
+
+ "so they report no cost rather than zero.",
|
|
139
|
+
},
|
|
115
140
|
{
|
|
116
141
|
name: "attach",
|
|
117
142
|
group: "runtime",
|
|
@@ -874,6 +899,14 @@ export const HERD_VERBS = [
|
|
|
874
899
|
{ name: "status", description: "what the herd is running on, and how many sessions",
|
|
875
900
|
synopsis: [["moshcode herd status [--json]", ""]],
|
|
876
901
|
flags: [["--json", "machine-readable", ""]] },
|
|
902
|
+
{ name: "cost", description: "what each session is spending, from the engines' own logs",
|
|
903
|
+
synopsis: [["moshcode herd cost [name] [--all] [--since 6h] [--json]", ""]],
|
|
904
|
+
flags: [
|
|
905
|
+
["--all", "every engine session on disk, herd or not", ""],
|
|
906
|
+
["--since <dur>", "how far back to look", "24h"],
|
|
907
|
+
["--watch [secs]", "re-read on an interval", "10"],
|
|
908
|
+
["--json", "machine-readable", ""],
|
|
909
|
+
] },
|
|
877
910
|
{ name: "start", description: "start a session and hand the prompt back",
|
|
878
911
|
synopsis: [["moshcode herd start <engine> [--name <slug>] [--agent] [args…]", ""]],
|
|
879
912
|
flags: [
|
|
@@ -994,6 +1027,8 @@ export const PIT_COMMANDS = [
|
|
|
994
1027
|
description: "sessions that keep running when you leave" },
|
|
995
1028
|
{ name: "ps", cli: "ps",
|
|
996
1029
|
description: "what the herd is running, and which one wants you" },
|
|
1030
|
+
{ name: "cost", args: "[name] [--all]", cli: "cost",
|
|
1031
|
+
description: "what the herd is spending, from the engines' own logs" },
|
|
997
1032
|
{ name: "attach", args: "<name>", cli: "attach",
|
|
998
1033
|
description: "step into a herd session (detach leaves it running)" },
|
|
999
1034
|
{ name: "kill", args: "<name…>", cli: "kill",
|