wity-cli 0.1.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/dist/cli.mjs ADDED
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env node
2
+ import { t as version } from "./package-7FOUqcPO.mjs";
3
+ import { C as cost, E as painter, F as CliError, I as EXIT, M as KEYS_PAGE, O as shouldColor, P as resolveBaseURL, S as cleanBlock, T as ms, a as checkStateLength, d as readStdin, f as findKey, g as checkKey, h as checkHealth, i as askFileSchema, j as writeJson, k as spinner, l as readHidden, m as openStore, n as sendGenerate, o as describeIssues, p as keyPrefix, r as sendSystemOne, u as readState, x as clean, y as refusedHint } from "./send-CGXuVi9Z.mjs";
4
+ import { a as scoreQuestion, c as codeLangs, d as printCode, f as systemOneTs, i as pickField, l as curl, n as choiceQuestion, o as renderAnswer, r as noulQuestion, s as renderFooter, t as ask, u as generateTs } from "./questions-eZ01WB1q.mjs";
5
+ import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
6
+ import { readFile, stat } from "node:fs/promises";
7
+ import { parse } from "yaml";
8
+ //#region src/io.ts
9
+ const processIo = (signal) => ({
10
+ env: process.env,
11
+ stdin: process.stdin,
12
+ stdout: process.stdout,
13
+ stderr: process.stderr,
14
+ signal
15
+ });
16
+ //#endregion
17
+ //#region src/commands/api-key.ts
18
+ /** Printable characters only, no spaces. The SDK refuses anything else. */
19
+ const KEY_PATTERN = /^[\x21-\x7E]+$/;
20
+ const envWarning = (ctx, text) => {
21
+ if (ctx.io.env.WITY_API_KEY?.trim()) ctx.io.stderr.write(` ${ctx.err("yellow", "▲")} ${text}\n\n`);
22
+ };
23
+ /** Shown only when it isn't the usual address, so people notice they're pointed somewhere else. */
24
+ const addressLine = (ctx) => ctx.baseURL === "https://wity-proxy-production-2c33.up.railway.app" ? "" : ` ${ctx.err("dim", "API".padEnd(9))} ${ctx.baseURL}\n`;
25
+ const when = (iso) => new Date(iso).toLocaleString("en-GB", {
26
+ dateStyle: "medium",
27
+ timeStyle: "short"
28
+ });
29
+ /**
30
+ * Check the key with the free /v1/models call, then save it. A key saved before is replaced.
31
+ * The key comes from the command line, stdin (--stdin), or a hidden prompt, in that order.
32
+ */
33
+ const setKey = async (ctx, opts, given) => {
34
+ const { io } = ctx;
35
+ if (given !== void 0 && opts.stdin) throw new CliError("Give the key on the command line or with --stdin, not both.", EXIT.usage);
36
+ const via = given !== void 0 ? "argument" : opts.stdin ? "token" : "paste";
37
+ let key;
38
+ if (via === "argument") key = (given ?? "").trim();
39
+ else if (via === "token") {
40
+ if (io.stdin.isTTY) throw new CliError("--stdin reads the key from stdin.", EXIT.usage, "Pipe it in: wity api-key set --stdin < key.txt");
41
+ key = (await readStdin(io.stdin)).trim();
42
+ } else {
43
+ io.stderr.write(`\n ${ctx.err("bold", "Save your Wity API key")}\n\n`);
44
+ io.stderr.write(` Create a key at ${ctx.err("cyan", KEYS_PAGE)} and paste it below.\n`);
45
+ io.stderr.write(` ${ctx.err("dim", "Nothing shows while you paste. Press Enter when done.")}\n\n`);
46
+ key = (await readHidden(io, " API key: ")).trim();
47
+ }
48
+ if (!key) throw new CliError("No key was given.", EXIT.usage);
49
+ if (!KEY_PATTERN.test(key)) throw new CliError("The key has spaces or unusual characters.", EXIT.usage, "Check that you copied the whole key and nothing else.");
50
+ const stop = spinner(io, "Checking the key…", "Still checking the key…");
51
+ let check;
52
+ try {
53
+ check = await checkKey(key, ctx.baseURL, io);
54
+ } finally {
55
+ stop();
56
+ }
57
+ if (check.status === 401) throw new CliError("Wity refused this key.", EXIT.auth, "Check that you copied all of it, and that it hasn't been revoked.");
58
+ if (check.status !== 200) throw new CliError(`Couldn't check the key. Wity answered ${check.status}.`, EXIT.error);
59
+ const store = await ctx.store();
60
+ await store.set({
61
+ key,
62
+ baseURL: ctx.baseURL,
63
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
64
+ via
65
+ });
66
+ io.stderr.write(`\n ${ctx.err("green", "✓")} ${ctx.err("bold", "Saved.")} The key works.\n\n`);
67
+ io.stderr.write(` ${ctx.err("dim", "Key".padEnd(9))} ${keyPrefix(key)}\n`);
68
+ io.stderr.write(` ${ctx.err("dim", "Saved in")} ${store.location}\n`);
69
+ io.stderr.write(addressLine(ctx));
70
+ io.stderr.write("\n");
71
+ envWarning(ctx, "WITY_API_KEY is set in this shell, and it wins over the saved key. Unset it to use the saved one.");
72
+ return EXIT.ok;
73
+ };
74
+ /** Remove the saved key from this computer. The key itself keeps working until it's revoked on the website. */
75
+ const removeKey = async (ctx) => {
76
+ const { io } = ctx;
77
+ const p = ctx.err;
78
+ const store = await ctx.store();
79
+ if (!await store.delete(ctx.baseURL)) {
80
+ io.stderr.write(`\n There's no saved key on this computer${ctx.baseURL === "https://wity-proxy-production-2c33.up.railway.app" ? "" : ` for ${ctx.baseURL}`}.\n\n`);
81
+ envWarning(ctx, "WITY_API_KEY is set in this shell, so commands keep using it. Remove it with: unset WITY_API_KEY");
82
+ return EXIT.ok;
83
+ }
84
+ io.stderr.write(`\n ${p("green", "✓")} ${p("bold", "Removed the key")} from ${store.location}.\n`);
85
+ io.stderr.write(` ${p("dim", `It still works until you revoke it at ${KEYS_PAGE}`)}\n\n`);
86
+ envWarning(ctx, "WITY_API_KEY is still set in this shell, so commands keep using it. Remove it with: unset WITY_API_KEY");
87
+ return EXIT.ok;
88
+ };
89
+ const showKey = async (ctx, opts) => {
90
+ const { io } = ctx;
91
+ const found = await findKey(ctx.baseURL, io.env, ctx.store);
92
+ if (!found) throw new CliError("No API key found.", EXIT.auth, "Run `wity api-key set`, or set WITY_API_KEY.");
93
+ const stop = opts.json ? () => {} : spinner(io, "Checking the key…", "Still checking the key…");
94
+ let check;
95
+ let problem;
96
+ try {
97
+ check = await checkKey(found.key, ctx.baseURL, io);
98
+ } catch (err) {
99
+ if (!(err instanceof CliError) || err.code === EXIT.cancelled) throw err;
100
+ problem = err.message;
101
+ } finally {
102
+ stop();
103
+ }
104
+ const works = check ? check.status === 200 : void 0;
105
+ if (opts.json || !io.stdout.isTTY) writeJson(io, {
106
+ api: ctx.baseURL,
107
+ key: keyPrefix(found.key),
108
+ source: found.source,
109
+ location: found.location,
110
+ saved_at: found.saved?.savedAt || null,
111
+ works: works ?? null
112
+ });
113
+ else {
114
+ const p = ctx.out;
115
+ const line = (label, value) => io.stdout.write(` ${p("dim", label.padEnd(9))} ${value}\n`);
116
+ const dot = works === true ? p("green", "●") : works === false ? p("red", "●") : p("yellow", "●");
117
+ const title = works === false ? "The key was refused" : "Wity API key";
118
+ io.stdout.write(`\n ${dot} ${p("bold", title)}\n\n`);
119
+ line("Key", keyPrefix(found.key));
120
+ line("From", `${found.location}${found.saved?.savedAt ? ` · saved ${when(found.saved.savedAt)}` : ""}`);
121
+ line("API", ctx.baseURL);
122
+ line("Status", works === true ? `${p("green", "works")} ${p("dim", `· checked in ${ms(check?.ms ?? 0)}`)}` : works === false ? p("red", "refused") : p("yellow", `not checked: ${problem ?? "no answer"}`));
123
+ io.stdout.write("\n");
124
+ if (works === false) io.stdout.write(` ${p("dim", refusedHint(found))}\n\n`);
125
+ }
126
+ if (works === false) return EXIT.auth;
127
+ if (works === void 0) return EXIT.error;
128
+ return EXIT.ok;
129
+ };
130
+ //#endregion
131
+ //#region src/commands/ask.ts
132
+ /** Question files are small. Anything bigger is a mistake. */
133
+ const MAX_FILE_BYTES = 1048576;
134
+ /** Read a YAML or JSON file (JSON is valid YAML). */
135
+ const readDataFile = async (path, what) => {
136
+ let size;
137
+ try {
138
+ size = (await stat(path)).size;
139
+ } catch {
140
+ throw new CliError(`Can't read the ${what} ${path}`, EXIT.usage);
141
+ }
142
+ if (size > MAX_FILE_BYTES) throw new CliError(`The ${what} is over 1 MB.`, EXIT.usage);
143
+ try {
144
+ return parse(await readFile(path, "utf8"));
145
+ } catch (err) {
146
+ throw new CliError(`The ${what} isn't valid YAML or JSON: ${clean(err.message).slice(0, 200)}`, EXIT.usage);
147
+ }
148
+ };
149
+ const askFile = async (ctx, path, flags) => {
150
+ const { io } = ctx;
151
+ const parsed = askFileSchema.safeParse(await readDataFile(path, "questions file"));
152
+ if (!parsed.success) throw new CliError(`The questions file has problems:\n ${describeIssues(parsed.error)}`, EXIT.usage);
153
+ const file = parsed.data;
154
+ if (flags.maxLatency !== void 0 && (!Number.isInteger(flags.maxLatency) || flags.maxLatency < 200 || flags.maxLatency > 12e4)) throw new CliError("--max-latency is a whole number of milliseconds, from 200 to 120000.", EXIT.usage);
155
+ let state = file.state;
156
+ if (state !== void 0 && (flags.text !== void 0 || flags.file !== void 0)) throw new CliError("The file already has a state. Leave out --text and --file, or remove state from the file.", EXIT.usage);
157
+ state ??= await readState(flags, io);
158
+ checkStateLength(state);
159
+ const reasoning = flags.reasoning ?? file.reasoning;
160
+ const maxLatency = flags.maxLatency ?? file.max_latency_ms;
161
+ const request = {
162
+ state,
163
+ questions: file.questions,
164
+ ...reasoning && { reasoning },
165
+ ...maxLatency !== void 0 && { max_latency_ms: maxLatency }
166
+ };
167
+ if (flags.dryRun) {
168
+ writeJson(io, request);
169
+ if (io.stderr.isTTY) io.stderr.write(` ${ctx.err("dim", "Dry run: nothing was sent.")}\n`);
170
+ return EXIT.ok;
171
+ }
172
+ const langs = codeLangs(flags.code);
173
+ if (langs) {
174
+ printCode(ctx, langs, systemOneTs(request, ctx.baseURL), curl("/v1/systemone", request, ctx.baseURL));
175
+ return EXIT.ok;
176
+ }
177
+ const { res, totalMs } = await sendSystemOne(ctx, request);
178
+ if (flags.field !== void 0) {
179
+ const [name = "", ...rest] = flags.field.split(".");
180
+ const answer = res.answers[name];
181
+ if (!answer) throw new CliError(`There's no question named "${clean(name)}".`, EXIT.usage, `Questions: ${Object.keys(res.answers).join(", ")}`);
182
+ io.stdout.write(`${rest.length > 0 ? pickField(answer, rest.join(".")) : JSON.stringify(answer)}\n`);
183
+ } else if (flags.json || !io.stdout.isTTY) writeJson(io, res);
184
+ else {
185
+ const p = ctx.out;
186
+ const lines = [""];
187
+ for (const [name, question] of Object.entries(file.questions)) {
188
+ const answer = res.answers[name];
189
+ if (!answer) continue;
190
+ lines.push(` ${p("dim", clean(name))} ${p("bold", clean(question.instructions))}`, "");
191
+ lines.push(...renderAnswer(answer, p, io.stdout.columns), "");
192
+ }
193
+ lines.push(renderFooter(res, totalMs, p), "");
194
+ io.stdout.write(`${lines.join("\n")}\n`);
195
+ }
196
+ return EXIT.ok;
197
+ };
198
+ //#endregion
199
+ //#region src/commands/doctor.ts
200
+ /** A CliError becomes a failed check. Anything else is a bug and is thrown. */
201
+ const attempt = async (name, run) => {
202
+ try {
203
+ return await run();
204
+ } catch (err) {
205
+ if (!(err instanceof CliError) || err.code === EXIT.cancelled) throw err;
206
+ return {
207
+ name,
208
+ state: "fail",
209
+ detail: err.message,
210
+ hint: err.hint
211
+ };
212
+ }
213
+ };
214
+ const doctor = async (ctx, opts) => {
215
+ const { io } = ctx;
216
+ const pretty = !opts.json && Boolean(io.stdout.isTTY);
217
+ const p = ctx.out;
218
+ const results = [];
219
+ const show = (result) => {
220
+ results.push(result);
221
+ if (!pretty) return;
222
+ const mark = result.state === "ok" ? p("green", "✓") : result.state === "fail" ? p("red", "✗") : p("dim", "–");
223
+ io.stdout.write(` ${mark} ${result.name.padEnd(15)} ${result.state === "skip" ? p("dim", result.detail) : result.detail}\n`);
224
+ if (result.hint) io.stdout.write(` ${" ".repeat(15)} ${p("dim", result.hint)}\n`);
225
+ };
226
+ if (pretty) io.stdout.write(`\n ${p("bold", "Wity doctor")}\n\n`);
227
+ show({
228
+ name: "Node.js",
229
+ state: "ok",
230
+ detail: process.versions.node
231
+ });
232
+ show({
233
+ name: "API address",
234
+ state: "ok",
235
+ detail: ctx.baseURL === "https://wity-proxy-production-2c33.up.railway.app" ? ctx.baseURL : `${ctx.baseURL} (custom)`
236
+ });
237
+ show(await attempt("Key storage", async () => {
238
+ return {
239
+ name: "Key storage",
240
+ state: "ok",
241
+ detail: (await ctx.store()).location
242
+ };
243
+ }));
244
+ const health = await attempt("Wity reachable", async () => {
245
+ const check = await checkHealth(ctx.baseURL, io);
246
+ return check.status === 200 ? {
247
+ name: "Wity reachable",
248
+ state: "ok",
249
+ detail: `answered in ${ms(check.ms)}`
250
+ } : {
251
+ name: "Wity reachable",
252
+ state: "fail",
253
+ detail: `the health check answered ${check.status}`
254
+ };
255
+ });
256
+ show(health);
257
+ let found;
258
+ /** No key, or a key Wity refused. Exits with the auth code, so scripts can tell it from a network problem. */
259
+ let authProblem = false;
260
+ show(await attempt("API key", async () => {
261
+ found = await findKey(ctx.baseURL, io.env, ctx.store);
262
+ authProblem = !found;
263
+ return found ? {
264
+ name: "API key",
265
+ state: "ok",
266
+ detail: `${keyPrefix(found.key)} from ${found.location}`
267
+ } : {
268
+ name: "API key",
269
+ state: "fail",
270
+ detail: "no key found",
271
+ hint: "Run `wity api-key set`, or set WITY_API_KEY."
272
+ };
273
+ }));
274
+ const using = found;
275
+ show(using && health.state === "ok" ? await attempt("Key accepted", async () => {
276
+ const check = await checkKey(using.key, ctx.baseURL, io);
277
+ authProblem = check.status === 401;
278
+ return check.status === 200 ? {
279
+ name: "Key accepted",
280
+ state: "ok",
281
+ detail: `yes, checked in ${ms(check.ms)}`
282
+ } : check.status === 401 ? {
283
+ name: "Key accepted",
284
+ state: "fail",
285
+ detail: "no, Wity refused it",
286
+ hint: refusedHint(using)
287
+ } : {
288
+ name: "Key accepted",
289
+ state: "fail",
290
+ detail: `couldn't tell, Wity answered ${check.status}`
291
+ };
292
+ }) : {
293
+ name: "Key accepted",
294
+ state: "skip",
295
+ detail: "skipped"
296
+ });
297
+ const failed = results.filter((result) => result.state === "fail");
298
+ if (opts.json || !io.stdout.isTTY) writeJson(io, {
299
+ ok: failed.length === 0,
300
+ checks: results
301
+ });
302
+ else io.stdout.write(`\n ${failed.length === 0 ? p("green", "All good.") : p("red", `${failed.length} check${failed.length > 1 ? "s" : ""} failed.`)}\n\n`);
303
+ if (failed.length === 0) return EXIT.ok;
304
+ if (authProblem) return EXIT.auth;
305
+ return EXIT.error;
306
+ };
307
+ //#endregion
308
+ //#region src/commands/generate.ts
309
+ /** State is optional here: some instructions need no input. Empty stdin means none. */
310
+ const readOptionalState = async (flags, ctx) => {
311
+ if (flags.text !== void 0 || flags.file !== void 0) return readState(flags, ctx.io);
312
+ if (ctx.io.stdin.isTTY) return void 0;
313
+ const piped = await readStdin(ctx.io.stdin);
314
+ if (piped.trim() === "") return void 0;
315
+ checkStateLength(piped);
316
+ return piped;
317
+ };
318
+ const footer = (res, totalMs, paint) => ` ${paint("dim", [
319
+ clean(res.model),
320
+ `${ms(res.metadata.elapsed_ms)} on Wity`,
321
+ `${ms(totalMs)} total`,
322
+ `${res.usage.input_tokens.toLocaleString("en")} input tokens`,
323
+ `${res.usage.output_tokens.toLocaleString("en")} written (free)`,
324
+ `≈ ${cost(res.usage.input_tokens)}`
325
+ ].join(" · "))}`;
326
+ const generate = async (ctx, instructions, flags) => {
327
+ const { io } = ctx;
328
+ if (instructions.trim() === "") throw new CliError("The instructions are empty.", EXIT.usage);
329
+ if (flags.maxTokens !== void 0 && (!Number.isInteger(flags.maxTokens) || flags.maxTokens < 1 || flags.maxTokens > 512)) throw new CliError("--max-tokens is a whole number from 1 to 512.", EXIT.usage);
330
+ let shape;
331
+ if (flags.shape !== void 0) {
332
+ const data = await readDataFile(flags.shape, "shape file");
333
+ if (typeof data !== "object" || data === null || Array.isArray(data) || data.type !== "object") throw new CliError("The shape must be a JSON Schema with \"type\": \"object\" at the top.", EXIT.usage);
334
+ shape = data;
335
+ }
336
+ const state = await readOptionalState(flags, ctx);
337
+ const request = {
338
+ ...state !== void 0 && { state },
339
+ instructions,
340
+ ...shape && { shape },
341
+ ...flags.maxTokens !== void 0 && { max_tokens: flags.maxTokens }
342
+ };
343
+ if (flags.dryRun) {
344
+ writeJson(io, request);
345
+ if (io.stderr.isTTY) io.stderr.write(` ${ctx.err("dim", "Dry run: nothing was sent.")}\n`);
346
+ return EXIT.ok;
347
+ }
348
+ const langs = codeLangs(flags.code);
349
+ if (langs) {
350
+ printCode(ctx, langs, generateTs(request, ctx.baseURL), curl("/v1/generate", request, ctx.baseURL));
351
+ return EXIT.ok;
352
+ }
353
+ const { res, totalMs } = await sendGenerate(ctx, request);
354
+ const cutOff = res.finish_reason === "length";
355
+ if (flags.json) writeJson(io, res);
356
+ else if (!io.stdout.isTTY) {
357
+ if (shape) {
358
+ if (res.value == null) throw new CliError("Wity was cut off before the JSON was complete.", EXIT.error, "Raise --max-tokens.");
359
+ io.stdout.write(`${JSON.stringify(res.value)}\n`);
360
+ } else io.stdout.write(`${cleanBlock(res.text)}\n`);
361
+ } else {
362
+ const p = ctx.out;
363
+ const lines = [
364
+ "",
365
+ ...(shape && res.value != null ? JSON.stringify(res.value, null, 2) : cleanBlock(res.text)).split("\n").map((line) => ` ${line}`),
366
+ ""
367
+ ];
368
+ if (cutOff) lines.push(` ${p("yellow", `▲ Cut off at ${flags.maxTokens ?? 128} tokens. Raise --max-tokens for the rest.`)}`, "");
369
+ lines.push(footer(res, totalMs, p), "");
370
+ io.stdout.write(`${lines.join("\n")}\n`);
371
+ }
372
+ if (cutOff && !flags.json && !io.stdout.isTTY) io.stderr.write(` ${ctx.err("yellow", "▲")} Cut off at max tokens. Raise --max-tokens for the rest.\n`);
373
+ if (cutOff && shape && res.value == null) return EXIT.error;
374
+ return EXIT.ok;
375
+ };
376
+ //#endregion
377
+ //#region src/context.ts
378
+ const makeCtx = (io, baseUrlFlag) => {
379
+ const baseURL = resolveBaseURL(baseUrlFlag, io.env);
380
+ let opened;
381
+ return {
382
+ io,
383
+ baseURL,
384
+ store: () => {
385
+ opened ??= openStore(io.env);
386
+ return opened;
387
+ },
388
+ out: painter(shouldColor(io.env, io.stdout)),
389
+ err: painter(shouldColor(io.env, io.stderr))
390
+ };
391
+ };
392
+ //#endregion
393
+ //#region src/main.ts
394
+ const number = (value) => {
395
+ const parsed = Number(value);
396
+ if (value.trim() === "" || !Number.isFinite(parsed)) throw new InvalidArgumentError("Not a number.");
397
+ return parsed;
398
+ };
399
+ const collect = (value, previous = []) => [...previous, value];
400
+ const askOptions = (command) => command.option("-t, --text <text>", "the text to judge").option("--file <path>", "read the text from a file (- for stdin)").addOption(new Option("-r, --reasoning <mode>", "when Wity thinks before answering").choices([
401
+ "off",
402
+ "auto",
403
+ "always"
404
+ ])).option("--max-latency <ms>", "cap the time per request, 200 to 120000 ms", number).option("--fail-under <n>", "exit with code 10 if the answer is under n", number).option("--field <path>", "print one field of the answer, like noul or probabilities.billing").option("--json", "print the full response as JSON (the default when piped)").option("--dry-run", "print the request without sending it").option("--code [lang]", "print the request as TypeScript and curl (or only ts, or curl) instead of sending it");
405
+ const EXAMPLES = `
406
+ Examples:
407
+ cat email.txt | wity noul "Is this spam?"
408
+ wity choice "Which team handles this?" -o billing="Payments and refunds" -o tech="Bugs" --file ticket.txt
409
+ wity score "How upset is the customer?" -l Calm -l Annoyed -l Angry --text "Where is my order?!"
410
+ wity noul "Is this safe to merge?" --file diff.txt --fail-under 0.8 && git merge
411
+ wity ask questions.yaml --file ticket.txt
412
+ wity generate "A one-line reply to this customer" --file ticket.txt
413
+
414
+ Exit codes:
415
+ 0 ok · 1 API or network error · 2 bad input (not billed) · 3 no API key, or the key was refused
416
+ 10 answered, but failed --fail-under or --expect · 130 cancelled
417
+ `;
418
+ const main = async (argv, io) => {
419
+ let code = EXIT.ok;
420
+ const program = new Command("wity");
421
+ const ctx = () => makeCtx(io, program.opts().baseUrl);
422
+ program.description("Ask Wity typed questions about any text, and get a probability for every answer.").version(version, "-v, --version", "print the version").helpOption("-h, --help", "show help").helpCommand("help [command]", "show help for a command").addOption(new Option("--base-url <url>", "API address (default: WITY_BASE_URL, then the Wity API)").hideHelp()).addHelpText("after", EXAMPLES).showHelpAfterError("(add --help for usage)").configureOutput({
423
+ writeOut: (text) => io.stdout.write(text),
424
+ writeErr: (text) => io.stderr.write(text),
425
+ outputError: (text, write) => write(text.replace(/wity_[\w-]{16,}/g, "wity_…"))
426
+ }).exitOverride().action(async () => {
427
+ if (!io.stdin.isTTY || !io.stdout.isTTY) program.help();
428
+ const { playground } = await import("./playground-Cn7EIrd7.mjs");
429
+ code = await playground(ctx(), "");
430
+ });
431
+ const setHint = "Run `wity api-key set <key>`, or `wity api-key set` to paste it at a hidden prompt.";
432
+ const keyCommand = program.command("api-key").description("save, show or remove your Wity API key").allowExcessArguments().allowUnknownOption().action((_opts, command) => {
433
+ if (command.args.length > 0) throw new CliError("`wity api-key` takes set, show or remove.", EXIT.usage, setHint);
434
+ command.help();
435
+ });
436
+ keyCommand.command("set").description("save an API key: give it here, or paste it at a hidden prompt").argument("[key]", "the API key (leave out to paste it at a prompt)").option("--stdin", "read the key from stdin, for CI and scripts").allowExcessArguments().allowUnknownOption().action(async (_key, opts, command) => {
437
+ const [key, ...rest] = command.args;
438
+ if (rest.length > 0 || key?.startsWith("-")) throw new CliError("`wity api-key set` takes only the key.", EXIT.usage, setHint);
439
+ code = await setKey(ctx(), opts, key);
440
+ });
441
+ keyCommand.command("show").description("show which key is in use, and check that it works").option("--json", "print as JSON").action(async (opts) => {
442
+ code = await showKey(ctx(), opts);
443
+ });
444
+ keyCommand.command("remove").description("remove the saved key from this computer").action(async () => {
445
+ code = await removeKey(ctx());
446
+ });
447
+ program.command("doctor").description("check the setup: key, storage, and the connection to Wity").option("--json", "print as JSON").action(async (opts) => {
448
+ code = await doctor(ctx(), opts);
449
+ });
450
+ askOptions(program.command("noul").description("ask a yes or no question. Prints the probability of yes").argument("<question>", "the question, like \"Is this spam?\"").option("--if-yes <text>", "what yes means (use with --if-no)").option("--if-no <text>", "what no means (use with --if-yes)")).action(async (question, opts) => {
451
+ code = await ask(ctx(), noulQuestion(question, opts.ifYes, opts.ifNo), opts);
452
+ });
453
+ askOptions(program.command("choice").description("pick one of several options. Prints a probability for each").argument("<question>", "the question, like \"Which team should handle this?\"").requiredOption("-o, --option <id=description>", "an option, repeat for each (2 to 256)", collect).option("--expect <id>", "exit with code 10 unless this option wins (or, with --fail-under, is at least n)")).action(async (question, opts) => {
454
+ code = await ask(ctx(), choiceQuestion(question, opts.option), opts, { expect: opts.expect });
455
+ });
456
+ askOptions(program.command("score").description("place the text on a scale of 2 to 10 levels").argument("<question>", "the question, like \"How upset is the customer?\"").requiredOption("-l, --level <text>", "a level, lowest first, repeat for each (2 to 10)", collect)).action(async (question, opts) => {
457
+ code = await ask(ctx(), scoreQuestion(question, opts.level), opts);
458
+ });
459
+ program.command("ask").description("ask several named questions about one text, from a YAML or JSON file").argument("<questions-file>", "the questions, in the API's shape (see README)").option("-t, --text <text>", "the text to judge (unless the file has a state)").option("--file <path>", "read the text from a file (- for stdin)").addOption(new Option("-r, --reasoning <mode>", "when Wity thinks before answering").choices([
460
+ "off",
461
+ "auto",
462
+ "always"
463
+ ])).option("--max-latency <ms>", "cap the time per request, 200 to 120000 ms", number).option("--field <path>", "print one value, like team.choice or urgent.noul").option("--json", "print the full response as JSON (the default when piped)").option("--dry-run", "print the request without sending it").option("--code [lang]", "print the request as TypeScript and curl (or only ts, or curl) instead of sending it").action(async (path, opts) => {
464
+ code = await askFile(ctx(), path, opts);
465
+ });
466
+ program.command("generate").description("write short text, or JSON that matches a shape").argument("<instructions>", "what to write, like \"A one-line reply to this customer\"").option("-t, --text <text>", "the text to write from (optional)").option("--file <path>", "read the text from a file (- for stdin)").option("--shape <file>", "a JSON Schema (JSON or YAML) the output must match").option("--max-tokens <n>", "the most tokens to write, 1 to 512 (default 128)", number).option("--json", "print the full response as JSON").option("--dry-run", "print the request without sending it").option("--code [lang]", "print the request as TypeScript and curl (or only ts, or curl) instead of sending it").action(async (instructions, opts) => {
467
+ code = await generate(ctx(), instructions, opts);
468
+ });
469
+ program.command("playground").description("try questions live in the terminal (also: wity with no arguments)").option("-t, --text <text>", "start with this text").option("--file <path>", "start with the text of this file").action(async (opts) => {
470
+ const text = opts.text !== void 0 || opts.file !== void 0 ? await readState(opts, io) : "";
471
+ const { playground } = await import("./playground-Cn7EIrd7.mjs");
472
+ code = await playground(ctx(), text);
473
+ });
474
+ program.command("mcp").description("run Wity as an MCP server for AI agents").command("serve").description("serve Wity's tools over stdio, for Claude Code, Claude Desktop, Cursor and others").option("--max-spend <usd>", "stop paid calls after this many dollars in one session", number).action(async (opts) => {
475
+ const { serveMcp } = await import("./mcp-DnqXX8ba.mjs");
476
+ code = await serveMcp(ctx(), opts);
477
+ });
478
+ try {
479
+ await program.parseAsync(argv, { from: "user" });
480
+ return code;
481
+ } catch (err) {
482
+ if (err instanceof CommanderError) return err.exitCode === 0 ? EXIT.ok : EXIT.usage;
483
+ const paint = painter(shouldColor(io.env, io.stderr));
484
+ if (err instanceof CliError) {
485
+ io.stderr.write(`\n ${paint("red", "✗")} ${err.message}\n`);
486
+ if (err.hint) io.stderr.write(` ${paint("dim", err.hint)}\n`);
487
+ io.stderr.write("\n");
488
+ return err.code;
489
+ }
490
+ const message = err instanceof Error ? err.message : String(err);
491
+ io.stderr.write(`\n ${paint("red", "✗")} Something went wrong: ${clean(message)}\n\n`);
492
+ return EXIT.error;
493
+ }
494
+ };
495
+ //#endregion
496
+ //#region src/cli.ts
497
+ const controller = new AbortController();
498
+ process.on("SIGINT", () => {
499
+ process.stderr.write("\x1B[?25h");
500
+ controller.abort();
501
+ setTimeout(() => process.exit(130), 500).unref();
502
+ });
503
+ process.stdout.on("error", (err) => {
504
+ if (err.code === "EPIPE") process.exit(process.exitCode ?? 0);
505
+ throw err;
506
+ });
507
+ process.exitCode = await main(process.argv.slice(2), processIo(controller.signal));
508
+ //#endregion
509
+ export {};