moshcode 0.24.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +580 -0
  3. package/bin/moshcode.mjs +674 -0
  4. package/bin/moshscript.mjs +29 -0
  5. package/examples/alive.mosh +6 -0
  6. package/examples/scripting-the-cli.mosh +21 -0
  7. package/examples/team-secrets.mosh +20 -0
  8. package/examples/templates/bun-caddy-sqlite/.env.example +14 -0
  9. package/examples/templates/bun-caddy-sqlite/Caddyfile +18 -0
  10. package/examples/templates/bun-caddy-sqlite/README.md +97 -0
  11. package/examples/templates/bun-caddy-sqlite/deploy/moshcode-dns.service +39 -0
  12. package/examples/templates/bun-caddy-sqlite/deploy/moshpit-service.service +38 -0
  13. package/examples/templates/bun-caddy-sqlite/package.json +15 -0
  14. package/examples/templates/bun-caddy-sqlite/src/db.ts +47 -0
  15. package/examples/templates/bun-caddy-sqlite/src/server.ts +44 -0
  16. package/examples/templates/bun-caddy-sqlite/template.json +10 -0
  17. package/examples/templates/caddy-proxy/Caddyfile +36 -0
  18. package/examples/templates/caddy-proxy/README.md +104 -0
  19. package/examples/templates/caddy-proxy/deploy/moshcode-dns.service +39 -0
  20. package/examples/templates/caddy-proxy/template.json +8 -0
  21. package/examples/templates/caddy-static/Caddyfile +16 -0
  22. package/examples/templates/caddy-static/README.md +90 -0
  23. package/examples/templates/caddy-static/deploy/moshcode-dns.service +39 -0
  24. package/examples/templates/caddy-static/site/index.html +11 -0
  25. package/examples/templates/caddy-static/template.json +8 -0
  26. package/install.sh +194 -0
  27. package/package.json +28 -0
  28. package/prd/0000-template.md +49 -0
  29. package/prd/0001-wrap-ugig-and-coinpay-clis.md +121 -0
  30. package/prd/0002-separate-agent-and-raw-engine-launches.md +113 -0
  31. package/prd/0003-cross-engine-mcp-and-skill-installation.md +165 -0
  32. package/prd/0004-moshscript-run-programmable-moshcode.md +344 -0
  33. package/prd/0005-hosted-moshpit-resolver.md +192 -0
  34. package/prd/0006-help.md +359 -0
  35. package/prd/0007-profullstack-site-init.md +1183 -0
  36. package/prd/README.md +26 -0
  37. package/src/ads.mjs +58 -0
  38. package/src/auth.mjs +193 -0
  39. package/src/cli-schema.mjs +533 -0
  40. package/src/cli.mjs +118 -0
  41. package/src/commands.mjs +259 -0
  42. package/src/completion.mjs +594 -0
  43. package/src/console.mjs +244 -0
  44. package/src/dns-system.mjs +404 -0
  45. package/src/dns.mjs +2872 -0
  46. package/src/doh-server.mjs +256 -0
  47. package/src/doh.mjs +218 -0
  48. package/src/engines.mjs +385 -0
  49. package/src/escalate.mjs +85 -0
  50. package/src/help.mjs +443 -0
  51. package/src/integrations.mjs +265 -0
  52. package/src/mcp-catalog.mjs +50 -0
  53. package/src/mcp.mjs +155 -0
  54. package/src/mirror.mjs +187 -0
  55. package/src/notify.mjs +86 -0
  56. package/src/open-url.mjs +34 -0
  57. package/src/parking-http.mjs +65 -0
  58. package/src/pins.mjs +190 -0
  59. package/src/pit-url.mjs +13 -0
  60. package/src/prd.mjs +341 -0
  61. package/src/pty.mjs +176 -0
  62. package/src/pwd.mjs +103 -0
  63. package/src/registry.mjs +37 -0
  64. package/src/release-install.mjs +191 -0
  65. package/src/runtime.mjs +161 -0
  66. package/src/selfupdate.mjs +215 -0
  67. package/src/serve.mjs +502 -0
  68. package/src/skills.mjs +93 -0
  69. package/src/tabs.mjs +144 -0
  70. package/src/templates.mjs +456 -0
  71. package/src/tools.mjs +231 -0
  72. package/src/trade.mjs +137 -0
  73. package/src/trust.mjs +712 -0
  74. package/src/tui.mjs +736 -0
  75. package/src/ui.mjs +49 -0
  76. package/src/uninstall.mjs +113 -0
  77. package/src/upgrade.mjs +217 -0
package/prd/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # PRDs
2
+
3
+ Product requirements documents for this repo, following the
4
+ [OpenPRD](https://github.com/profullstack/logicsrc/blob/master/docs/openprd.md)
5
+ standard β€” a numbered, committed proposal collection (like a BIP/EIP/DIP
6
+ process).
7
+
8
+ Each PRD is one file: `NNNN-slug.md`. `0000-template.md` is the template.
9
+ Lifecycle: **Draft β†’ Review β†’ Accepted β†’ Final** (or Rejected / Withdrawn /
10
+ Superseded). Status lives in each file's front matter.
11
+
12
+ Start one with `moshcode prd "<idea>"` (TUI: `/prd`).
13
+
14
+ ## Index
15
+
16
+ <!-- PRD-INDEX:START -->
17
+ | # | Title | Status |
18
+ |---|---|---|
19
+ | [0001](0001-wrap-ugig-and-coinpay-clis.md) | Wrap the UGig and CoinPay CLIs | Accepted |
20
+ | [0002](0002-separate-agent-and-raw-engine-launches.md) | Separate autonomous agent and raw engine launches | Accepted |
21
+ | [0003](0003-cross-engine-mcp-and-skill-installation.md) | Install MCP servers and skills across every engine at once | Accepted |
22
+ | [0004](0004-moshscript-run-programmable-moshcode.md) | moshscript β€” a scriptable /run for driving all of moshcode programmatically | Accepted |
23
+ | [0005](0005-hosted-moshpit-resolver.md) | A hosted Moshpit resolver, for the devices that cannot run the bridge | Draft |
24
+ | [0006](0006-help.md) | --help | Draft |
25
+ | [0007](0007-profullstack-site-init.md) | Generate batteries-included Profullstack sites for Moshpit names | Draft |
26
+ <!-- PRD-INDEX:END -->
package/src/ads.mjs ADDED
@@ -0,0 +1,58 @@
1
+ // The MOTD ad printed under the startup banner.
2
+ //
3
+ // CrawlProof's terminal format is *fetched*, not embedded: the endpoint returns
4
+ // a finished pure-ASCII box as text/plain, already wrapped to `cols`, so the
5
+ // pit only has to print it. Everything here is best-effort and silent β€” a slow
6
+ // or unreachable ad server must never delay or dirty the prompt.
7
+ const AD_ENDPOINT = "https://crawlproof.com/api/ads/motd";
8
+ const SLOT = "a138e9c5-4b7d-4eaa-8b2d-cb3a2dd9382f";
9
+ // 76, not 72. The click URL carries `?s=moshcode` and runs 71 chars, which
10
+ // can't fit inside a 72-wide box β€” the renderer then drops it below the
11
+ // footer as a dangling line. 76 keeps it on one line inside the box and still
12
+ // fits an 80-column terminal. Override with MOSHCODE_AD_COLS if you must.
13
+ const COLS = Number(process.env.MOSHCODE_AD_COLS) || 76;
14
+ const TIMEOUT_MS = 1500;
15
+
16
+ /**
17
+ * Fetch the MOTD block. Resolves to the text to print, or null when it's
18
+ * disabled, unavailable, or doesn't look like the box we asked for.
19
+ *
20
+ * Kick this off BEFORE rendering the banner and await it after: the startup
21
+ * text takes long enough to print that the ad usually costs no visible time.
22
+ */
23
+ export async function fetchMotdAd({ cols = COLS, timeoutMs = TIMEOUT_MS, signal } = {}) {
24
+ if (process.env.MOSHCODE_NO_ADS) return null;
25
+ const url = `${AD_ENDPOINT}?slot=${encodeURIComponent(SLOT)}&cols=${cols}&src=moshcode`;
26
+ const abort = new AbortController();
27
+ const timer = setTimeout(() => abort.abort(), timeoutMs);
28
+ signal?.addEventListener?.("abort", () => abort.abort(), { once: true });
29
+ try {
30
+ const res = await fetch(url, { signal: abort.signal, headers: { accept: "text/plain" } });
31
+ if (!res.ok) return null;
32
+ const body = await res.text();
33
+ return sanitizeAd(body, cols);
34
+ } catch {
35
+ return null; // offline, slow, blocked β€” the pit opens either way
36
+ } finally {
37
+ clearTimeout(timer);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Strip anything that could repaint the terminal and drop lines wider than the
43
+ * box we asked for. The server sanitizes too, but this is the last gate before
44
+ * untrusted advertiser copy reaches someone's screen β€” an escape sequence here
45
+ * could move the cursor, clear the display, or hide what the pit prints next.
46
+ */
47
+ export function sanitizeAd(text, cols = COLS) {
48
+ if (typeof text !== "string") return null;
49
+ const lines = text
50
+ .split("\n")
51
+ // eslint-disable-next-line no-control-regex
52
+ .map((line) => line.replace(/[^\x20-\x7e]/g, "").trimEnd())
53
+ .filter((line, i, arr) => !(line === "" && (i === 0 || i === arr.length - 1)));
54
+ if (!lines.length) return null;
55
+ if (lines.some((line) => line.length > cols + 8)) return null; // not the box we asked for
56
+ const out = lines.join("\n").trim();
57
+ return out || null;
58
+ }
package/src/auth.mjs ADDED
@@ -0,0 +1,193 @@
1
+ // `moshcode login` β€” browser OAuth-style flow (authorization code + PKCE +
2
+ // loopback) that authenticates against the moshcode app (app.moshcode.sh) and
3
+ // stores an API token locally. That token is what moshscript's notify()/ask()
4
+ // use to reach you (see src/notify.mjs), so a single `moshcode login` wires up
5
+ // the whole human-in-the-loop for scripts too.
6
+ import http from "node:http";
7
+ import crypto from "node:crypto";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { spawn } from "node:child_process";
12
+
13
+ const API = () => (process.env.MOSHCODE_API || "https://app.moshcode.sh").replace(/\/+$/, "");
14
+ const CREDS_DIR = path.join(os.homedir(), ".moshcode");
15
+ export const credsPath = path.join(CREDS_DIR, "credentials.json");
16
+
17
+ const b64url = (buf) => Buffer.from(buf).toString("base64url");
18
+
19
+ /** Stored credentials, or null. { api, token, email } */
20
+ export function loadCreds() {
21
+ try { return JSON.parse(fs.readFileSync(credsPath, "utf8")); } catch { return null; }
22
+ }
23
+ export function saveCreds(creds) {
24
+ fs.mkdirSync(CREDS_DIR, { recursive: true });
25
+ fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
26
+ fs.chmodSync(credsPath, 0o600);
27
+ }
28
+
29
+ /**
30
+ * True when the loopback flow can't work: the browser that opens the authorize
31
+ * URL is on a *different* machine than this process, so the callback to
32
+ * 127.0.0.1 lands on that machine and never reaches our listener. SSH sessions
33
+ * and headless boxes (a droplet, a container, CI) are exactly that case β€” there
34
+ * the device-code flow is the only one that can finish.
35
+ */
36
+ export function isRemoteShell() {
37
+ const forced = String(process.env.MOSHCODE_LOGIN || "").toLowerCase();
38
+ if (forced === "browser") return false;
39
+ if (forced === "device") return true;
40
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT) return true;
41
+ // No display server on Linux β†’ nothing here can open a browser for us.
42
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
43
+ return false;
44
+ }
45
+
46
+ function openBrowser(url) {
47
+ const [cmd, args] =
48
+ process.platform === "darwin" ? ["open", [url]]
49
+ : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]]
50
+ : ["xdg-open", [url]];
51
+ try { const c = spawn(cmd, args, { stdio: "ignore", detached: true }); c.on("error", () => {}); c.unref(); } catch { /* print fallback */ }
52
+ }
53
+
54
+ const donePage = (msg) =>
55
+ `<!doctype html><meta charset=utf-8><body style="background:#070806;color:#edf2e4;font-family:ui-monospace,monospace;text-align:center;padding:16vh 24px">
56
+ <div style="font-size:2rem">🀘</div><h1 style="color:#a6ff1a">${msg}</h1><p>Return to your terminal. You can close this tab.</p></body>`;
57
+
58
+ /** Run the login flow. Returns { email } on success; throws on failure/timeout. */
59
+ export function login({ timeoutMs = 180000 } = {}) {
60
+ const verifier = b64url(crypto.randomBytes(32));
61
+ const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
62
+ const state = b64url(crypto.randomBytes(16));
63
+
64
+ return new Promise((resolve, reject) => {
65
+ const server = http.createServer(async (req, res) => {
66
+ const url = new URL(req.url, "http://127.0.0.1");
67
+ if (url.pathname !== "/callback") { res.writeHead(404).end(); return; }
68
+ const err = url.searchParams.get("error");
69
+ const code = url.searchParams.get("code");
70
+ const gotState = url.searchParams.get("state");
71
+ try {
72
+ if (err) throw new Error(`authorization denied (${err})`);
73
+ if (!code || gotState !== state) throw new Error("bad authorization response (state mismatch)");
74
+ const tokRes = await fetch(`${API()}/cli/token`, {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json" },
77
+ body: JSON.stringify({ code, code_verifier: verifier }),
78
+ });
79
+ if (!tokRes.ok) throw new Error(`token exchange failed (${tokRes.status})`);
80
+ const tok = await tokRes.json();
81
+ saveCreds({ api: API(), token: tok.access_token, email: tok.user?.email || null, id: tok.user?.id });
82
+ res.writeHead(200, { "content-type": "text/html" }).end(donePage("you're in 🀘"));
83
+ server.close();
84
+ resolve({ email: tok.user?.email || null });
85
+ } catch (e) {
86
+ res.writeHead(400, { "content-type": "text/html" }).end(donePage("login failed β€” check the terminal"));
87
+ server.close();
88
+ reject(e);
89
+ }
90
+ });
91
+
92
+ server.listen(0, "127.0.0.1", () => {
93
+ const port = server.address().port;
94
+ const redirect = `http://127.0.0.1:${port}/callback`;
95
+ const authUrl = `${API()}/cli/authorize?` + new URLSearchParams({
96
+ redirect_uri: redirect, state, code_challenge: challenge, code_challenge_method: "S256",
97
+ name: `moshcode cli @ ${os.hostname()}`,
98
+ });
99
+ console.log(`\nπŸ”‘ opening your browser to authorize the moshcode CLI…`);
100
+ console.log(` if it doesn't open, visit:\n ${authUrl}\n`);
101
+ openBrowser(authUrl);
102
+ });
103
+
104
+ const timer = setTimeout(() => { server.close(); reject(new Error("login timed out β€” run `moshcode login` again")); }, timeoutMs);
105
+ server.on("close", () => clearTimeout(timer));
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Device-code login (headless / CI): no local browser or loopback needed. Prints
111
+ * a short code + URL; you approve it in ANY browser; the CLI polls until done.
112
+ */
113
+ export async function loginDevice({ open = true } = {}) {
114
+ const startRes = await fetch(`${API()}/cli/device/code`, {
115
+ method: "POST",
116
+ headers: { "content-type": "application/json" },
117
+ body: JSON.stringify({ name: `moshcode cli @ ${os.hostname()}` }),
118
+ });
119
+ if (!startRes.ok) throw new Error(`couldn't start device login (${startRes.status})`);
120
+ const d = await startRes.json();
121
+
122
+ console.log(`\nπŸ”‘ to log in, open: ${d.verification_uri}`);
123
+ console.log(` and enter code: \x1b[1m\x1b[38;5;154m${d.user_code}\x1b[0m\n`);
124
+ if (open) openBrowser(d.verification_uri_complete);
125
+
126
+ const interval = Math.max(2, d.interval || 5) * 1000;
127
+ const deadline = Date.now() + (d.expires_in || 600) * 1000;
128
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
129
+
130
+ console.log(" waiting for you to authorize…");
131
+ for (;;) {
132
+ if (Date.now() > deadline) throw new Error("code expired β€” run `moshcode login --device` again");
133
+ await sleep(interval);
134
+ let data;
135
+ try {
136
+ const r = await fetch(`${API()}/cli/device/token`, {
137
+ method: "POST",
138
+ headers: { "content-type": "application/json" },
139
+ body: JSON.stringify({ device_code: d.device_code }),
140
+ });
141
+ data = await r.json();
142
+ } catch { continue; } // transient network β€” keep polling
143
+ if (data.access_token) {
144
+ saveCreds({ api: API(), token: data.access_token, email: data.user?.email || null, id: data.user?.id });
145
+ return { email: data.user?.email || null };
146
+ }
147
+ if (data.error === "access_denied") throw new Error("authorization denied");
148
+ if (data.error === "expired_token") throw new Error("code expired β€” run `moshcode login --device` again");
149
+ // authorization_pending / slow_down β†’ keep waiting
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Pick the login flow that can actually finish here. Over SSH or on a headless
155
+ * box the loopback callback is undeliverable (it hits the *browser's* machine),
156
+ * so fall back to the device code instead of stranding you on a dead
157
+ * 127.0.0.1 URL. `device: true` / `browser: true` force one either way.
158
+ */
159
+ export async function loginAuto({ device = false, browser = false } = {}) {
160
+ const remote = !browser && (device || isRemoteShell());
161
+ if (!remote) return login();
162
+ if (!device) {
163
+ console.log(`\nπŸ–₯ remote/headless shell β€” using the device-code flow.`);
164
+ console.log(` (a loopback callback would open on the browser's machine, not this one.)`);
165
+ }
166
+ return loginDevice({ open: !isRemoteShell() });
167
+ }
168
+
169
+ /** Print who is logged in (verified against the app). */
170
+ export async function whoami() {
171
+ const creds = loadCreds();
172
+ if (!creds?.token) { console.log("not logged in β€” run: moshcode login"); return; }
173
+ try {
174
+ const res = await fetch(`${creds.api || API()}/api/me`, { headers: { authorization: `Bearer ${creds.token}` } });
175
+ if (res.status === 401) { console.log("session expired β€” run: moshcode login"); return; }
176
+ // Any other error status still has a body, and it isn't an account β€” reading
177
+ // it as one prints a made-up identity for a session the app just refused.
178
+ if (!res.ok) {
179
+ console.log(`${creds.email || "logged in"} @ ${creds.api || API()} (couldn't verify β€” the app returned ${res.status})`);
180
+ return;
181
+ }
182
+ const me = await res.json();
183
+ console.log(`${me.email || me.name || "moshcoder"} 🀘 (${me.credits ?? "?"} credits) @ ${creds.api || API()}`);
184
+ } catch {
185
+ console.log(`${creds.email || "logged in"} @ ${creds.api || API()} (couldn't reach the app to verify)`);
186
+ }
187
+ }
188
+
189
+ /** Forget local credentials. */
190
+ export function logout() {
191
+ try { fs.rmSync(credsPath); console.log("logged out 🀘"); }
192
+ catch { console.log("already logged out"); }
193
+ }