moshcode 0.38.0 → 0.40.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
@@ -43,6 +43,8 @@ or miss one that does. A test fails the build when it drifts.
43
43
  | `moshcode login` | account | authenticate with app.moshcode.sh |
44
44
  | `moshcode whoami` | account | show the logged-in account |
45
45
  | `moshcode logout` | account | clear the logged-in account |
46
+ | `moshcode save` | account | save this machine's pit settings to your account |
47
+ | `moshcode load` | account | bring your saved pit settings onto this machine |
46
48
  | `moshcode console` | account | serve or connect to the browser terminal |
47
49
  | `moshcode dns` | hosting | resolve Moshpit names on this machine |
48
50
  | `moshcode doh` | hosting | run the DNS-over-HTTPS resolver |
@@ -543,6 +545,24 @@ an equity's score, and each response ships the `caveats` that say so. Prices are
543
545
  Alpaca's US venue alone and can differ materially from other exchanges. Research
544
546
  aid, not advice — and like `stocks`, nothing under `crypto` can place an order.
545
547
 
548
+ ### Aliases (`/alias`)
549
+
550
+ The pit is a prompt you sit at all day, so it lets you name the lines you keep
551
+ retyping. An alias runs in `$SHELL` unless it starts with `/`, in which case it
552
+ is a pit command:
553
+
554
+ ```text
555
+ /alias set gs "git status" # then /gs — and /gs -sb appends to it
556
+ /alias set cx "/agents codex" # a pit command, not a shell one
557
+ /alias # what is defined
558
+ /alias rm gs
559
+ ```
560
+
561
+ They live in `~/.moshcode/aliases.json` (owner-only, like the history file) and
562
+ survive between sessions. A name that is already a pit command, an engine, or a
563
+ tool is refused rather than shadowed — built-ins are dispatched first, so such
564
+ an alias would never run.
565
+
546
566
  ### Social posting from the pit
547
567
 
548
568
  The pit can hand a prepared post to Bluesky or Nostr without storing either
@@ -561,6 +581,52 @@ event, and publishes it to the displayed relays. Both flows leave the final
561
581
  confirmation in the browser. If the pit is remote or headless, `/post` prints
562
582
  the composer URL instead.
563
583
 
584
+ ## Settings sync (`/save` and `/load`)
585
+
586
+ Your pit becomes yours by accretion — a dozen aliases, herd rules you tuned until
587
+ the roster stopped lying to you. All of it lives in `~/.moshcode` on one machine,
588
+ which is why every new laptop, container and droplet used to feel like someone
589
+ else's prompt.
590
+
591
+ `/save` pushes that configuration to your `app.moshcode.sh` account. `/load`
592
+ brings it down onto any machine you have run `/login` on.
593
+
594
+ ```sh
595
+ moshcode save # push this machine's settings (pit: /save)
596
+ moshcode save --dry-run # what would go up, and stop
597
+
598
+ # on the new box
599
+ moshcode login
600
+ moshcode load # pull them down (pit: /load)
601
+ moshcode load --dry-run # the per-file plan, changing nothing
602
+ ```
603
+
604
+ What syncs is an allowlist, not a directory walk:
605
+
606
+ | file | what it is |
607
+ |---|---|
608
+ | `~/.moshcode/aliases.json` | your pit aliases (`/alias`) |
609
+ | `~/.moshcode/herd/rules.json` | herd state-detection overrides |
610
+
611
+ What never syncs, by name: `credentials.json` (the account token this very
612
+ feature authenticates with), `herd/sessions.json` (live state pinned to one tmux
613
+ server), `sync.json`, and the `pkg/` binary cache. Engine configuration
614
+ (`~/.claude.json` and friends) is deliberately left alone — those files carry
615
+ provider API keys.
616
+
617
+ Nothing is overwritten quietly:
618
+
619
+ - Each save is a numbered **revision**. `/save` sends the revision it last agreed
620
+ on, and the app refuses the write if another machine has saved since — you get
621
+ told, with `/load` and `/save --force` as the two ways out.
622
+ - `/load` refuses to replace a settings file you edited since this machine last
623
+ synced, and names it. `--force` overrides.
624
+ - The last ten revisions are kept. See them, and which machine each came from, at
625
+ [app.moshcode.sh/settings/sync](https://app.moshcode.sh/settings/sync) — where
626
+ you can also promote an older revision or delete the lot.
627
+
628
+ Both verbs take `--json`, so a provisioning script can act on the result.
629
+
564
630
  ## Browser terminal (`moshcode console`)
565
631
 
566
632
  A real terminal in the browser — arrow keys, history, full-screen TUIs — because
package/bin/moshcode.mjs CHANGED
@@ -26,6 +26,7 @@ import { canOpenBrowser, openBrowser } from "../src/open-url.mjs";
26
26
  import { locate, tilde } from "../src/pwd.mjs";
27
27
  import { createPrd, listPrds, authoringPrompt } from "../src/prd.mjs";
28
28
  import { loginAuto, whoami, logout } from "../src/auth.mjs";
29
+ import { loadCommand, saveCommand } from "../src/settings-sync.mjs";
29
30
  import { tui } from "../src/tui.mjs";
30
31
  import { consoleCommand } from "../src/console.mjs";
31
32
  import { herdCommand, herdStart, splitDetachArgs } from "../src/herd-cli.mjs";
@@ -609,6 +610,8 @@ async function main() {
609
610
  return;
610
611
  }
611
612
  if (cmd === "logout") { logout(); return; }
613
+ if (cmd === "save") { process.exitCode = await saveCommand(rest); return; }
614
+ if (cmd === "load") { process.exitCode = await loadCommand(rest); return; }
612
615
  if (cmd === "run") {
613
616
  let max = 3, dryRun = false;
614
617
  let optionsEnded = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.38.0",
3
+ "version": "0.40.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": {
@@ -0,0 +1,132 @@
1
+ ---
2
+ openprd: "0.2"
3
+ id: "0010"
4
+ title: "Sync the pit's settings to your moshcode.sh account"
5
+ status: Draft
6
+ authors:
7
+ - anthony@profullstack.com
8
+ created: 2026-08-11
9
+ updated: 2026-08-11
10
+ repo: https://github.com/moshcoder/moshcode
11
+ discussion:
12
+ implementation: src/settings-sync.mjs · apps/pwa/src/routes/settings-sync.mjs
13
+ tags: account, settings, sync
14
+ supersedes:
15
+ superseded-by:
16
+ ---
17
+
18
+ ## Problem
19
+
20
+ A pit becomes yours by accretion. You add `/alias set gs "git status"`, then a
21
+ dozen more; you tune the herd's rules until it stops calling a working agent
22
+ blocked. None of it is in a repo, none of it is in a dotfile anyone syncs, and
23
+ all of it lives in `~/.moshcode` on exactly one machine.
24
+
25
+ So the second machine is a stranger. A new laptop, a fresh container, a droplet
26
+ you SSH into to babysit an agent, a reinstall after a disk swap — each one starts
27
+ from nothing, and the muscle memory built at the first prompt does not work at
28
+ the second. People already log in to `app.moshcode.sh` (`/login`) for approvals,
29
+ notifications and the session mirror, so the account that could hold this
30
+ configuration is already there and already paired with every machine.
31
+
32
+ ## Goals
33
+
34
+ - Moving to a new machine costs `/login` and `/load`, not an afternoon of
35
+ remembering what you had.
36
+ - A person can see what is stored on their account, and delete it, from the web.
37
+ - Nobody is ever surprised by a settings overwrite — not from another machine,
38
+ and not over their own uncommitted edits.
39
+ - No credential, key or token is ever part of what syncs, and that fact is
40
+ enforced by a test rather than by care.
41
+
42
+ ## Non-Goals
43
+
44
+ - Continuous or background sync. Settings are edited by a person at a moment they
45
+ can name; a daemon that pushes silently is a daemon that overwrites silently.
46
+ - Syncing engine configuration (`~/.claude.json`, `~/.codex`, MCP registrations).
47
+ Those files carry provider API keys and are owned by other tools' schemas.
48
+ - Syncing machine state: live herd sessions, the package cache, shell history.
49
+ None of it means anything on a different box.
50
+ - Merging. Two divergent settings files are resolved by a person choosing one,
51
+ not by a three-way merge of someone's aliases.
52
+
53
+ ## Users
54
+
55
+ - **The multi-machine moshcoder** — laptop, desktop, a dev box, and a container
56
+ per project. Wants the same prompt everywhere.
57
+ - **The reinstaller** — new OS, same person. Wants their aliases back.
58
+ - **The team lead** — one account, several machines, and a strong preference for
59
+ never explaining to a colleague why their aliases disappeared.
60
+
61
+ ## Requirements
62
+
63
+ - R1 [P0] `/save` (and `moshcode save`) uploads this machine's pit settings to the
64
+ logged-in account. `/load` (`moshcode load`) brings them back down.
65
+ - R2 [P0] What syncs is an allowlist, not a directory walk: `aliases.json` and
66
+ `herd/rules.json` today. `credentials.json`, `herd/sessions.json`, `sync.json`
67
+ and `pkg/` are named as never-synced and asserted in tests.
68
+ - R3 [P0] Each save is a numbered revision. `/save` sends the revision it last
69
+ agreed on and the app refuses the write if the account has moved past it, so
70
+ two machines cannot silently erase one another.
71
+ - R4 [P0] `/load` refuses to overwrite a settings file that changed locally since
72
+ the last sync, and names the file. `--force` overrides; `--dry-run` shows the
73
+ per-file plan and writes nothing.
74
+ - R5 [P0] Every path in a downloaded snapshot is re-checked against the allowlist
75
+ before anything is written. A snapshot is data from the network, and an
76
+ unchecked path in it makes `/load` a remote write primitive.
77
+ - R6 [P1] The app keeps the last ten revisions, shows them at
78
+ `/settings/sync` with the machine and time each came from, and can promote an
79
+ older one to current.
80
+ - R7 [P1] `--json` on both verbs, so a provisioning script can act on the result.
81
+ - R8 [P1] A snapshot records which engines and tools the source machine had
82
+ installed. `/load` names the missing ones as a suggestion; it never installs.
83
+ - R9 [P2] Not logged in, session expired, nothing saved yet, conflict: each is a
84
+ sentence naming the command that resolves it (`/login`, `/save`, `/load`,
85
+ `--force`).
86
+
87
+ ## UX Notes
88
+
89
+ ```
90
+ mosh ▸ /save
91
+ ✓ saved 2 files to you@example.com (revision 3)
92
+ aliases.json pit aliases
93
+ herd/rules.json herd state rules
94
+ on another machine: `/login` then `/load`
95
+
96
+ mosh ▸ /load # on the new box
97
+ loaded revision 3 from dev — 2 files written
98
+ added aliases.json
99
+ added herd/rules.json
100
+ that machine also had codex, gh — `/install <name>` to match it
101
+
102
+ mosh ▸ /load # after editing aliases locally
103
+ 1 local file changed since this machine last synced:
104
+ aliases.json
105
+ `/save` to keep them, `/load --force` to replace them, `/load --dry-run` to see the difference
106
+ ```
107
+
108
+ The pit never blocks on this: both verbs are one request and some printing, so
109
+ readline keeps the prompt. `~/.moshcode/sync.json` remembers the revision and a
110
+ per-file digest — that digest is what separates "someone else saved" from "you
111
+ edited this five minutes ago", which want opposite answers.
112
+
113
+ ## Success Metrics
114
+
115
+ - A fresh machine reaches a familiar prompt in two commands (`/login`, `/load`).
116
+ - Zero settings-loss reports: every destructive path is either refused or
117
+ recoverable from `/settings/sync`.
118
+ - No credential ever appears in a stored snapshot (asserted, not audited).
119
+
120
+ ## Risks & Open Questions
121
+
122
+ - **Scope creep into secrets.** The most-requested next file will be an engine
123
+ config that holds an API key. Holding the line — settings, never credentials —
124
+ is what keeps `/load` safe to run on a machine you share.
125
+ - **Ten revisions is a guess.** Cheap to raise; it exists so a bad `/save` from
126
+ the wrong machine is recoverable at all.
127
+ - **A snapshot version bump.** Handled by refusing to read a newer snapshot and
128
+ naming `moshcode upgrade`, rather than by guessing at a shape this build has
129
+ never seen.
130
+ - **Should `/load` be able to pick a revision?** The app stores ten and the web
131
+ page can promote one, which covers recovery without adding a flag that takes a
132
+ number. Open if people ask for `--revision`.
package/prd/README.md CHANGED
@@ -25,4 +25,5 @@ Start one with `moshcode prd "<idea>"` (TUI: `/prd`).
25
25
  | [0007](0007-profullstack-site-init.md) | Generate batteries-included Profullstack sites for Moshpit names | Draft |
26
26
  | [0008](0008-ticker-research-and-plugin-marketplace.md) | Bring equity research into the pit, and ship the pit's slash commands as a plugin | Draft |
27
27
  | [0009](0009-persistent-agent-runtime.md) | Keep the herd alive — a persistent runtime, semantic agent state, and one control surface for humans and agents | Accepted |
28
+ | [0010](0010-cloud-settings-sync.md) | Sync the pit's settings to your moshcode.sh account | Draft |
28
29
  <!-- PRD-INDEX:END -->
@@ -0,0 +1,160 @@
1
+ // Named shortcuts for whatever you type at the mosh prompt.
2
+ //
3
+ // The pit is a prompt people sit at all day, and the things they retype are
4
+ // their own: `git status`, `pnpm -r test`, `/agents claude --resume`. Shell
5
+ // aliases can't help — the pit is not a shell, and `!git status` is exactly the
6
+ // keystrokes an alias is supposed to save. So the pit keeps its own.
7
+ //
8
+ // An alias is a name and a line. The line is a shell command unless it starts
9
+ // with `/`, in which case it is a pit command:
10
+ //
11
+ // /alias set gs "git status" → /gs runs `$SHELL -c "git status"`
12
+ // /alias set cc "/agents claude" → /cc opens claude autonomously
13
+ //
14
+ // Shell-by-default because that is what the prompt is mostly asked for, and the
15
+ // leading slash is already how the pit spells its own verbs — so the rule reads
16
+ // the same way the rest of the pit does rather than being a new convention.
17
+ //
18
+ // Anything the pit can dispatch is fair game as a value, which is what keeps
19
+ // this from needing to grow a type: a bookmarklet or a URL becomes an alias the
20
+ // day the pit gets a verb that opens one, with no change here.
21
+ import fs from "node:fs";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+
25
+ /** Owner-only, and for the same reason ~/.moshcode_history is: values are
26
+ * whatever was typed, and people alias commands that carry tokens. */
27
+ const FILE_MODE = 0o600;
28
+
29
+ const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
30
+
31
+ /** A value long enough to be a pasted mistake rather than a command. */
32
+ const MAX_VALUE = 4096;
33
+
34
+ /**
35
+ * How many times one line may expand before the pit gives up.
36
+ *
37
+ * Aliases can name aliases (`/alias set st "/gs --short"`), which is useful and
38
+ * also the one way to write a loop: two aliases naming each other would spin
39
+ * the dispatch loop forever. Ten is far past any chain a person builds on
40
+ * purpose.
41
+ */
42
+ export const MAX_EXPANSIONS = 10;
43
+
44
+ /** Where the aliases live. Derived per call so tests can move $HOME. */
45
+ export function aliasFile() {
46
+ return path.join(os.homedir(), ".moshcode", "aliases.json");
47
+ }
48
+
49
+ /**
50
+ * Every alias, as a plain name → line map.
51
+ *
52
+ * A file that is missing, unreadable, or not the shape we wrote reads as "no
53
+ * aliases" rather than throwing: this is called on the dispatch path for every
54
+ * unrecognised command, and a hand-edited file with a stray comma must not take
55
+ * the prompt down with it. Entries whose value is not a string are dropped for
56
+ * the same reason.
57
+ */
58
+ export function loadAliases() {
59
+ let raw;
60
+ try { raw = fs.readFileSync(aliasFile(), "utf8"); }
61
+ catch { return {}; }
62
+ let parsed;
63
+ try { parsed = JSON.parse(raw); }
64
+ catch { return {}; }
65
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
66
+ const out = {};
67
+ for (const [name, value] of Object.entries(parsed)) {
68
+ if (typeof value === "string" && value.trim()) out[name.toLowerCase()] = value;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /** Write the map back, creating ~/.moshcode if this is the first alias. */
74
+ function saveAliases(aliases) {
75
+ const file = aliasFile();
76
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
77
+ // Sorted so the file reads like a list rather than like insertion order, and
78
+ // so hand edits produce a small diff.
79
+ const ordered = Object.fromEntries(Object.keys(aliases).sort().map((k) => [k, aliases[k]]));
80
+ fs.writeFileSync(file, `${JSON.stringify(ordered, null, 2)}\n`, { mode: FILE_MODE });
81
+ // `mode` only applies at creation, so an existing file keeps whatever the
82
+ // umask gave it. Tighten every write, the way the history file does.
83
+ try { fs.chmodSync(file, FILE_MODE); } catch { /* best effort */ }
84
+ }
85
+
86
+ /** The name as it is stored, or "" for anything that cannot be one. */
87
+ export function normalizeName(name) {
88
+ const clean = String(name ?? "").trim().toLowerCase().replace(/^\//, "");
89
+ return NAME_RE.test(clean) ? clean : "";
90
+ }
91
+
92
+ /** One alias's line, or null. */
93
+ export function getAlias(name) {
94
+ const key = normalizeName(name);
95
+ if (!key) return null;
96
+ const aliases = loadAliases();
97
+ return Object.hasOwn(aliases, key) ? aliases[key] : null;
98
+ }
99
+
100
+ /**
101
+ * Define an alias. Returns { ok, error, name, value, previous }.
102
+ *
103
+ * `isReserved` asks the pit whether a name is already its own — a command, an
104
+ * engine, a tool. A predicate rather than a list because the dispatcher decides
105
+ * that by resolving, aliases included, and a list copied out of the rosters
106
+ * here would be a second answer that drifts from the first. A colliding name is
107
+ * refused rather than shadowed: built-ins are checked first, so an alias named
108
+ * `agents` would be silently dead, and a shortcut that does nothing is worse
109
+ * than one that was never accepted.
110
+ */
111
+ export function setAlias(name, value, { isReserved = () => false } = {}) {
112
+ const key = normalizeName(name);
113
+ if (!key) {
114
+ return { ok: false, error: `"${name}" isn't a usable alias name — letters, digits, . _ - and it must start with a letter or digit` };
115
+ }
116
+ if (isReserved(key)) {
117
+ return { ok: false, error: `/${key} is already a pit command, engine, or tool — pick another name` };
118
+ }
119
+ const line = String(value ?? "").trim();
120
+ if (!line) return { ok: false, error: "an alias needs something to run" };
121
+ if (line.includes("\n")) return { ok: false, error: "an alias is a single line" };
122
+ if (line.length > MAX_VALUE) return { ok: false, error: `that value is ${line.length} characters — the cap is ${MAX_VALUE}` };
123
+
124
+ const aliases = loadAliases();
125
+ const previous = Object.hasOwn(aliases, key) ? aliases[key] : null;
126
+ aliases[key] = line;
127
+ try { saveAliases(aliases); }
128
+ catch (e) { return { ok: false, error: `can't write ${aliasFile()}: ${e.message}` }; }
129
+ return { ok: true, name: key, value: line, previous };
130
+ }
131
+
132
+ /** Forget one. Returns { ok, error, name, value }. */
133
+ export function removeAlias(name) {
134
+ const key = normalizeName(name);
135
+ const aliases = loadAliases();
136
+ if (!key || !Object.hasOwn(aliases, key)) {
137
+ return { ok: false, error: `no alias named "${String(name ?? "").replace(/^\//, "")}"` };
138
+ }
139
+ const value = aliases[key];
140
+ delete aliases[key];
141
+ try { saveAliases(aliases); }
142
+ catch (e) { return { ok: false, error: `can't write ${aliasFile()}: ${e.message}` }; }
143
+ return { ok: true, name: key, value };
144
+ }
145
+
146
+ /**
147
+ * The line an alias becomes, with anything else the user typed appended.
148
+ *
149
+ * Appended rather than substituted, the way a shell alias behaves: `/gs -sb` is
150
+ * `git status -sb`. `args` is the raw remainder of the typed line, not the
151
+ * tokenized parts, so the user's own quoting survives into `$SHELL -c`.
152
+ *
153
+ * The `!` is what routes a bare value to the shell — the pit already reads a
154
+ * leading `!` as "run this in $SHELL", so an alias does not need a second path
155
+ * through it.
156
+ */
157
+ export function expandAlias(value, args = "") {
158
+ const line = `${String(value).trim()}${args ? ` ${args}` : ""}`;
159
+ return /^[/!]/.test(line) ? line : `!${line}`;
160
+ }
@@ -278,6 +278,43 @@ export const CORE_CLI_COMMANDS = [
278
278
  synopsis: [["moshcode logout", ""]],
279
279
  seeAlso: ["login"],
280
280
  },
281
+ {
282
+ name: "save",
283
+ group: "account",
284
+ description: "save this machine's pit settings to your account",
285
+ synopsis: [["moshcode save [--dry-run] [--force] [--json]", ""]],
286
+ flags: [
287
+ ["--dry-run", "list what would be saved and stop", ""],
288
+ ["--force", "save even if another machine saved after this one last synced", ""],
289
+ ["--json", "machine-readable result", ""],
290
+ ],
291
+ examples: [
292
+ ["moshcode save", "push aliases + herd rules to app.moshcode.sh"],
293
+ ["moshcode save --dry-run", "what would go up"],
294
+ ],
295
+ seeAlso: ["load", "login", "alias"],
296
+ note: "aliases (~/.moshcode/aliases.json) and herd rules (~/.moshcode/herd/rules.json). "
297
+ + "credentials, live herd state and the package cache are never included. "
298
+ + "each save is a numbered revision; the last ten are kept at app.moshcode.sh/settings/sync.",
299
+ },
300
+ {
301
+ name: "load",
302
+ group: "account",
303
+ description: "bring your saved pit settings onto this machine",
304
+ synopsis: [["moshcode load [--dry-run] [--force] [--json]", ""]],
305
+ flags: [
306
+ ["--dry-run", "show the per-file plan and change nothing", ""],
307
+ ["--force", "overwrite local settings that changed since the last sync", ""],
308
+ ["--json", "machine-readable result", ""],
309
+ ],
310
+ examples: [
311
+ ["moshcode load", "on a new machine, right after moshcode login"],
312
+ ["moshcode load --dry-run", "which files would change"],
313
+ ],
314
+ seeAlso: ["save", "login", "alias"],
315
+ note: "refuses rather than overwriting a local file you edited since the last sync — "
316
+ + "`moshcode save` to keep it, or --force to replace it.",
317
+ },
281
318
  {
282
319
  name: "console",
283
320
  group: "account",
@@ -891,10 +928,32 @@ export const PIT_COMMANDS = [
891
928
  { name: "whoami", cli: "whoami", description: "who this machine is logged in as" },
892
929
  // Dispatched since forever and missing from /help until now.
893
930
  { name: "logout", cli: "logout", description: "clear the logged-in account" },
931
+ { name: "save", args: "[--dry-run] [--force]", cli: "save",
932
+ description: "save this pit's settings to your moshcode.sh account" },
933
+ { name: "load", args: "[--dry-run] [--force]", cli: "load",
934
+ description: "bring your saved settings onto this machine" },
894
935
  { name: "pwd", aliases: ["where"], cli: "pwd",
895
936
  description: "show the current dir + git repo/branch/origin" },
896
937
  { name: "shell", aliases: ["sh"], args: "[cmd]", pitOnly: true,
897
938
  description: "drop into $SHELL (exit → back to the pit); also !cmd" },
939
+ { name: "alias", aliases: ["aliases"], args: 'set <name> "<cmd>" | list | get | rm', pitOnly: true,
940
+ description: "name a line you keep retyping; /<name> runs it",
941
+ synopsis: [
942
+ ['/alias set <name> "<command>"', "define one (also: /alias <name> \"<command>\")"],
943
+ ["/alias [list] [--json]", "every alias"],
944
+ ["/alias get <name>", "what one expands to"],
945
+ ["/alias rm <name>", "forget one"],
946
+ ],
947
+ examples: [
948
+ ['/alias set gs "git status"', "then /gs — and /gs -sb appends"],
949
+ // Deliberately not `cc`: that one is already how the pit spells claude,
950
+ // so the example would print a refusal for anyone who typed it.
951
+ ['/alias set cx "/agents codex"', "a pit command, not a shell one"],
952
+ ["/alias rm gs", ""],
953
+ ],
954
+ note: "the command runs in $SHELL unless it starts with / — then it is a pit command. "
955
+ + "Aliases live in ~/.moshcode/aliases.json and cannot shadow a pit command, engine, or tool.",
956
+ },
898
957
  { name: "help", aliases: ["?", "h"], args: "[command]", pitOnly: true,
899
958
  description: "this, or one command in detail" },
900
959
  { name: "quit", aliases: ["exit", "q"], pitOnly: true,