moshcode 0.67.0 → 0.69.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.
@@ -0,0 +1,212 @@
1
+ // `/shorten <url>` — mint a short link on the pit, and get `/f/<code>` back.
2
+ //
3
+ // The pit hands out long URLs constantly: a session mirror, an approval, a
4
+ // name's site, a release asset. The place they get pasted is a terminal, a chat
5
+ // line, a slide or a QR code, where a 140-character URL wraps and breaks in
6
+ // half. So this asks the registry for a short one and prints it.
7
+ //
8
+ // Everything here is one HTTP call to pit.moshcode.sh — the registry owns the
9
+ // codes, because a short link that only worked from the laptop that minted it
10
+ // would not be a link at all. The command is thin on purpose: parse, call,
11
+ // print, and be honest about what came back.
12
+ //
13
+ // Authenticated, always. An anonymous shortener is an open redirector with a
14
+ // database, which is the thing phishing kits are built out of; the account is
15
+ // what makes a link revocable and its owner findable.
16
+
17
+ import { loadCreds } from "./auth.mjs";
18
+ import { acid, ash, bone, err, info, ok } from "./ui.mjs";
19
+
20
+ /** Where the codes live. The registry, not the app — see the note above. */
21
+ export const DEFAULT_REGISTRY_BASE = "https://pit.moshcode.sh";
22
+
23
+ function registryBase(env = process.env) {
24
+ return String(env.MOSHPIT_REGISTRY || env.MOSHCODE_PIT || DEFAULT_REGISTRY_BASE).replace(/\/+$/, "");
25
+ }
26
+
27
+ /** The token `moshcode login` wrote, or one set in the environment. */
28
+ export function apiToken(env = process.env, creds = loadCreds) {
29
+ return env.MOSHCODE_API_KEY || creds()?.token || "";
30
+ }
31
+
32
+ /**
33
+ * Split `/shorten` into what it was asked to do.
34
+ *
35
+ * A bare URL is the whole point of the command, so it needs no verb: `/shorten
36
+ * https://…` shortens, and only `list` and `rm` are spelled out. Flags are
37
+ * pulled out first so `--name` can sit anywhere, which is where people put it.
38
+ *
39
+ * @param {string[]} argv
40
+ */
41
+ export function parseArgs(argv = []) {
42
+ const args = (Array.isArray(argv) ? argv : []).map(String);
43
+ const json = args.includes("--json");
44
+ let name = null;
45
+ const positional = [];
46
+
47
+ for (let i = 0; i < args.length; i += 1) {
48
+ const arg = args[i];
49
+ if (arg === "--json") continue;
50
+ if (arg === "--name" || arg === "-n") { name = args[i + 1] ?? null; i += 1; continue; }
51
+ if (arg.startsWith("--name=")) { name = arg.slice("--name=".length); continue; }
52
+ positional.push(arg);
53
+ }
54
+
55
+ const first = (positional[0] || "").toLowerCase();
56
+ if (!positional.length) return { verb: "help", json, name };
57
+ if (first === "list" || first === "ls") return { verb: "list", json, name };
58
+ if (first === "rm" || first === "delete" || first === "del") {
59
+ return { verb: "rm", code: positional[1] || "", json, name };
60
+ }
61
+ // Anything else is the URL. Deliberately not validated here: the registry has
62
+ // the one implementation of what may be shortened (lib/moshpit-links.mjs),
63
+ // and a second, looser copy in the client is how the two drift apart.
64
+ return { verb: "shorten", url: positional[0], json, name };
65
+ }
66
+
67
+ /**
68
+ * One authenticated call to the registry, with the failures a person can act on.
69
+ *
70
+ * Every non-2xx is turned into `{ ok: false, error }` rather than thrown: this
71
+ * runs at a prompt someone is sitting in front of, and a stack trace over a
72
+ * 401 tells them nothing about the `moshcode login` that fixes it.
73
+ */
74
+ async function call(path, { method = "GET", body = null, token, base, fetchImpl = fetch } = {}) {
75
+ if (!token) {
76
+ return { ok: false, needsAuth: true, error: "not logged in — run `/login` first" };
77
+ }
78
+ let response;
79
+ try {
80
+ response = await fetchImpl(`${base}${path}`, {
81
+ method,
82
+ headers: {
83
+ authorization: `Bearer ${token}`,
84
+ ...(body ? { "content-type": "application/json" } : {}),
85
+ },
86
+ ...(body ? { body: JSON.stringify(body) } : {}),
87
+ });
88
+ } catch (error) {
89
+ return { ok: false, error: `${base} unreachable: ${error.message}` };
90
+ }
91
+
92
+ let payload = null;
93
+ try { payload = await response.json(); } catch { payload = null; }
94
+
95
+ if (response.status === 401) {
96
+ return { ok: false, needsAuth: true, error: "the registry rejected the credentials — run `/login`" };
97
+ }
98
+ if (!response.ok) {
99
+ return { ok: false, error: payload?.error || `the registry said ${response.status}` };
100
+ }
101
+ return { ok: true, status: response.status, body: payload ?? {} };
102
+ }
103
+
104
+ /** Mint one. Returns the link the registry stored, existing or new. */
105
+ export async function shorten(url, {
106
+ name = null, env = process.env, token = apiToken(env), fetchImpl = fetch,
107
+ } = {}) {
108
+ return call("/api/moshpit/links", {
109
+ method: "POST",
110
+ body: { url, ...(name ? { name } : {}) },
111
+ token,
112
+ base: registryBase(env),
113
+ fetchImpl,
114
+ });
115
+ }
116
+
117
+ /** What this account has minted. */
118
+ export async function listLinks({ env = process.env, token = apiToken(env), fetchImpl = fetch } = {}) {
119
+ return call("/api/moshpit/links", { token, base: registryBase(env), fetchImpl });
120
+ }
121
+
122
+ /** Take one down. */
123
+ export async function removeLink(code, { env = process.env, token = apiToken(env), fetchImpl = fetch } = {}) {
124
+ return call(`/api/moshpit/links/${encodeURIComponent(code)}`, {
125
+ method: "DELETE", token, base: registryBase(env), fetchImpl,
126
+ });
127
+ }
128
+
129
+ /**
130
+ * How to run this, spelled the way the caller reached it.
131
+ *
132
+ * The pit writes its verbs with a slash and the CLI does not, and printing the
133
+ * wrong one is a usage line that does not work when pasted back — `/games` does
134
+ * the same thing for the same reason.
135
+ */
136
+ function usage(out, prefix) {
137
+ const lines = [
138
+ ["<url>", "mint a short link — /f/<code> on the pit"],
139
+ ["<url> --name <name>", "file it under a moshpit name you hold"],
140
+ ["list", "every link you have minted, newest first"],
141
+ ["rm <code>", "take one down"],
142
+ ].map(([args, text]) => [`${prefix} ${args}`, text]);
143
+ // The column is measured rather than fixed: `moshcode shorten` is twice as
144
+ // wide as `/shorten`, and a hardcoded one leaves the longest line unaligned
145
+ // in whichever spelling was not the one it was chosen for.
146
+ const width = Math.max(...lines.map(([invocation]) => invocation.length)) + 2;
147
+
148
+ out(info("usage:"));
149
+ for (const [invocation, text] of lines) {
150
+ out(` ${acid(invocation)}${ash(" ".repeat(width - invocation.length) + text)}`);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * `/shorten` in the pit, and `moshcode shorten` on the command line.
156
+ *
157
+ * @param {string[]} argv
158
+ * @param {{out?: (s: string) => void, err?: (s: string) => void, env?: object,
159
+ * token?: string, prefix?: string, fetchImpl?: typeof fetch}} [io]
160
+ * @returns {Promise<number>} exit code
161
+ */
162
+ export async function shortenCommand(argv = [], io = {}) {
163
+ const out = io.out || ((s) => console.log(s));
164
+ const say = io.err || ((s) => console.error(s));
165
+ const env = io.env || process.env;
166
+ const token = io.token ?? apiToken(env);
167
+ const prefix = io.prefix || "/shorten";
168
+ const fetchImpl = io.fetchImpl || fetch;
169
+ const parsed = parseArgs(argv);
170
+
171
+ if (parsed.verb === "help") {
172
+ usage(out, prefix);
173
+ return 1;
174
+ }
175
+
176
+ if (parsed.verb === "list") {
177
+ const result = await listLinks({ env, token, fetchImpl });
178
+ if (!result.ok) { say(err(result.error)); return 1; }
179
+ const links = result.body.links || [];
180
+ if (parsed.json) { out(JSON.stringify(links, null, 2)); return 0; }
181
+ if (!links.length) {
182
+ out(info(`no short links yet — ${prefix} <url> mints one`));
183
+ return 0;
184
+ }
185
+ for (const link of links) {
186
+ const hits = `${link.hits} hit${link.hits === 1 ? "" : "s"}`;
187
+ out(` ${acid(link.short)} ${ash("→")} ${bone(link.url)}`);
188
+ out(` ${ash(`${hits}${link.name ? ` · ${link.name}` : ""}`)}`);
189
+ }
190
+ return 0;
191
+ }
192
+
193
+ if (parsed.verb === "rm") {
194
+ if (!parsed.code) { say(err(`usage: ${prefix} rm <code>`)); return 1; }
195
+ const result = await removeLink(parsed.code, { env, token, fetchImpl });
196
+ if (!result.ok) { say(err(result.error)); return 1; }
197
+ if (parsed.json) { out(JSON.stringify(result.body, null, 2)); return 0; }
198
+ out(ok(`took down /f/${result.body.code ?? parsed.code}`));
199
+ return 0;
200
+ }
201
+
202
+ const result = await shorten(parsed.url, { name: parsed.name, env, token, fetchImpl });
203
+ if (!result.ok) { say(err(result.error)); return 1; }
204
+ if (parsed.json) { out(JSON.stringify(result.body, null, 2)); return 0; }
205
+
206
+ // Say when a code came back rather than being made. Shortening is idempotent
207
+ // per account, and someone who ran it twice should see why the code is the
208
+ // one they already have instead of wondering whether the second call worked.
209
+ out(ok(`${acid(result.body.short)} ${ash("→")} ${bone(result.body.url)}`));
210
+ if (result.body.created === false) out(info("already shortened — same code as last time"));
211
+ return 0;
212
+ }
package/src/tools.mjs CHANGED
@@ -310,6 +310,32 @@ export const TOOLS = {
310
310
  // place to hear it than an install that succeeded.
311
311
  install: { cmd: "npm", args: ["install", "-g", "@alchemy/cli"] },
312
312
  },
313
+ elevenlabs: {
314
+ desc: "ElevenLabs — build, configure and deploy Eleven Agents (plus voices, TTS, dubbing, and the rest of the API)",
315
+ bin: "elevenlabs",
316
+ // A workflow tool rather than an engine, despite the name. ElevenLabs calls
317
+ // these "agents", but they are conversational voice agents you configure and
318
+ // deploy to their platform: `elevenlabs agents push/pull/list` is an API
319
+ // client that runs one request and exits. `/agents` promises to hand the
320
+ // terminal to a live session, and there is no session here to hand it, so
321
+ // this belongs next to the other workflow CLIs. Docs:
322
+ // https://elevenlabs.io/docs/eleven-agents/operate/cli
323
+ //
324
+ // Authenticate once with `moshcode elevenlabs auth login` — a PKCE OAuth
325
+ // flow that stores the credential in the system keyring (falling back to a
326
+ // file under ~/.config), rather than an env var to keep exporting.
327
+ //
328
+ // The published package is a tiny Node shim; the real CLI is a native
329
+ // binary shipped per platform as an OPTIONAL dependency
330
+ // (@elevenlabs/cli-linux-x64 and friends). That is why the install spec is
331
+ // the plain global install with no flags: an install run with
332
+ // `--omit=optional` or `--no-optional` still succeeds and still puts
333
+ // `elevenlabs` on PATH, but every invocation then dies with "the platform
334
+ // package is not installed". `npm install -g` is idempotent and the CLI
335
+ // ships no updater of its own, so this doubles as the upgrade path and
336
+ // there is deliberately no `upgrade` key.
337
+ install: { cmd: "npm", args: ["install", "-g", "@elevenlabs/cli"] },
338
+ },
313
339
  };
314
340
 
315
341
  /** Resolve a name to `[key, tool]`, or null. */
package/src/tui.mjs CHANGED
@@ -11,6 +11,7 @@ import { ENGINES, agentLaunchArgs, resolveEngine, engineStatus, openSession } fr
11
11
  import { TOOLS, resolveTool, toolStatus, openTool, readToolAliases, toolsWithAliases } from "./tools.mjs";
12
12
  import { tradeArgs, tradeUsage } from "./trade.mjs";
13
13
  import { postSocial, socialRoster } from "./socials.mjs";
14
+ import { shortenCommand } from "./shorten.mjs";
14
15
  import { runUpgrade } from "./upgrade.mjs";
15
16
  import { locate, tilde } from "./pwd.mjs";
16
17
  import { createPrd, listPrds, authoringPrompt } from "./prd.mjs";
@@ -1110,6 +1111,12 @@ export async function tui() {
1110
1111
  rl = mkrl();
1111
1112
  continue;
1112
1113
  }
1114
+ // `/shorten` renders in the pit rather than handing the terminal over: it
1115
+ // is one call to the registry and one line back, the same as `/stocks`.
1116
+ if (cmd === "shorten" || cmd === "short" || cmd === "link") {
1117
+ await shortenCommand(rest, { prefix: `/${cmd}` });
1118
+ continue;
1119
+ }
1113
1120
  if (cmd === "socials" || cmd === "social") {
1114
1121
  printSocials();
1115
1122
  continue;