moshcode 0.58.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 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 five verbs people reach
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
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
- /** Print who is logged in (verified against the app). */
170
- export async function whoami({ json = false } = {}) {
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
- if (json) {
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
- if (json) {
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 prints a made-up identity for a session the app just refused.
203
+ // it as one reports a made-up identity for a session the app just refused.
210
204
  if (!res.ok) {
211
- if (json) {
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
- if (json) {
226
- printJson({
227
- status: "authenticated",
228
- verified: true,
229
- api,
230
- user: {
231
- id: me.id ?? creds.id ?? null,
232
- email: me.email ?? creds.email ?? null,
233
- name: me.name ?? null,
234
- credits: me.credits ?? null,
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
- if (json) {
242
- printJson({
243
- status: "unreachable",
244
- verified: false,
245
- api,
246
- user: localUser,
247
- error: { type: "network" },
248
- });
249
- } else {
250
- console.log(`${creds.email || "logged in"} @ ${api} (couldn't reach the app to verify)`);
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
 
@@ -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",