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.
@@ -0,0 +1,183 @@
1
+ import { t as version } from "./package-7FOUqcPO.mjs";
2
+ import { C as cost, D as pct, F as CliError, I as EXIT, N as PRICE_PER_MILLION_INPUT_USD, _ as explain, a as checkStateLength, c as MAX_STATE_CHARS, s as questionSchema, t as requireKey, v as makeClient, x as clean } from "./send-CGXuVi9Z.mjs";
3
+ import { z } from "zod";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ //#region src/mcp.ts
7
+ const UNCALIBRATED = "Probabilities rank how sure Wity is. They aren't calibrated odds.";
8
+ const text = z.string().min(1).max(MAX_STATE_CHARS).describe("The text to judge: an email, a ticket, a diff, a document…");
9
+ const reasoning = z.enum([
10
+ "off",
11
+ "auto",
12
+ "always"
13
+ ]).optional().describe("off is fastest. auto (the default) thinks only when a question needs it. always thinks first.");
14
+ const done = (summary, data) => ({ content: [{
15
+ type: "text",
16
+ text: `${summary}\n\n${JSON.stringify(data)}`
17
+ }] });
18
+ const failed = (err) => {
19
+ return {
20
+ isError: true,
21
+ content: [{
22
+ type: "text",
23
+ text: err instanceof CliError ? [err.message, err.hint].filter(Boolean).join(" ") : "Something went wrong."
24
+ }]
25
+ };
26
+ };
27
+ const usage = (res) => `${res.usage.input_tokens.toLocaleString("en")} input tokens ≈ ${cost(res.usage.input_tokens)}`;
28
+ /** One line about an answer, for the agent to read before the JSON. */
29
+ const summarize = (res) => Object.entries(res.answers).map(([name, answer]) => {
30
+ if (answer.type === "noul") return `${name}: yes ${pct(answer.noul)}`;
31
+ if (answer.type === "score") return `${name}: score ${answer.score.toFixed(2)}`;
32
+ return `${name}: ${clean(answer.choice)} (${pct(answer.probabilities[answer.choice] ?? 0)})`;
33
+ }).join(" · ");
34
+ const createMcpServer = (ctx, found, opts) => {
35
+ const server = new McpServer({
36
+ name: "wity",
37
+ version
38
+ });
39
+ const client = makeClient(found.key, ctx.baseURL, ctx.io);
40
+ let spentTokens = 0;
41
+ /** Refuse once the session has spent --max-spend. Checked before each paid call. */
42
+ const guard = () => {
43
+ const spent = spentTokens * PRICE_PER_MILLION_INPUT_USD / 1e6;
44
+ if (opts.maxSpendUsd !== void 0 && spent >= opts.maxSpendUsd) throw new CliError(`This session reached its spending cap of $${opts.maxSpendUsd}.`, EXIT.error, "Restart the MCP server to reset it, or raise --max-spend.");
45
+ };
46
+ const systemOne = async (request) => {
47
+ try {
48
+ guard();
49
+ checkStateLength(request.state);
50
+ const res = await client.systemOne(request, { signal: ctx.io.signal });
51
+ spentTokens += res.usage.input_tokens;
52
+ return done(`${summarize(res)} · ${usage(res)}. ${UNCALIBRATED}`, res);
53
+ } catch (err) {
54
+ return failed(err instanceof CliError ? err : explain(err, found));
55
+ }
56
+ };
57
+ const request = (state, question, mode) => ({
58
+ state,
59
+ questions: { answer: question },
60
+ ...mode && { reasoning: mode }
61
+ });
62
+ const readOnly = {
63
+ readOnlyHint: true,
64
+ openWorldHint: true
65
+ };
66
+ server.registerTool("wity_noul", {
67
+ title: "Yes or no, with a probability",
68
+ description: `Ask Wity a yes-or-no question about a text. Returns the probability of yes, from 0 to 1. ${UNCALIBRATED} Billed per input token (a few hundred tokens cost well under a cent).`,
69
+ inputSchema: {
70
+ text,
71
+ question: z.string().min(1).describe("A yes-or-no question, like 'Is this email a phishing attempt?'"),
72
+ yes_means: z.string().min(1).optional().describe("What yes means. Give no_means too."),
73
+ no_means: z.string().min(1).optional().describe("What no means. Give yes_means too."),
74
+ reasoning
75
+ },
76
+ annotations: readOnly
77
+ }, async ({ text: state, question, yes_means, no_means, reasoning: mode }) => {
78
+ if (yes_means === void 0 !== (no_means === void 0)) return failed(new CliError("Give both yes_means and no_means, or neither.", EXIT.usage));
79
+ const criteria = yes_means && no_means ? {
80
+ true: yes_means,
81
+ false: no_means
82
+ } : void 0;
83
+ return systemOne(request(state, {
84
+ type: "noul",
85
+ instructions: question,
86
+ ...criteria && { criteria }
87
+ }, mode));
88
+ });
89
+ server.registerTool("wity_choice", {
90
+ title: "Pick one option, with probabilities",
91
+ description: `Ask Wity to pick one of 2 to 256 options for a text, like routing a ticket or labelling an email. Returns the winner and a probability for every option. ${UNCALIBRATED}`,
92
+ inputSchema: {
93
+ text,
94
+ question: z.string().min(1).describe("What to decide, like 'Which team should handle this ticket?'"),
95
+ options: z.record(z.string().min(1), z.string()).refine((o) => Object.keys(o).length >= 2 && Object.keys(o).length <= 256, "needs 2 to 256 options").describe("Option ids mapped to what each one means, like {\"billing\": \"Payments and refunds\"}."),
96
+ reasoning
97
+ },
98
+ annotations: readOnly
99
+ }, async ({ text: state, question, options, reasoning: mode }) => systemOne(request(state, {
100
+ type: "choice",
101
+ instructions: question,
102
+ criteria: options
103
+ }, mode)));
104
+ server.registerTool("wity_score", {
105
+ title: "Place a text on a scale",
106
+ description: `Ask Wity to place a text on a scale of 2 to 10 levels, lowest first. Returns the expected level (it can fall between levels) and a probability per level. ${UNCALIBRATED}`,
107
+ inputSchema: {
108
+ text,
109
+ question: z.string().min(1).describe("What to rate, like 'How upset is the customer?'"),
110
+ levels: z.array(z.string().min(1)).min(2).max(10).describe("The levels, lowest first, like ['Calm', 'Annoyed', 'Angry']."),
111
+ reasoning
112
+ },
113
+ annotations: readOnly
114
+ }, async ({ text: state, question, levels, reasoning: mode }) => systemOne(request(state, {
115
+ type: "score",
116
+ instructions: question,
117
+ criteria: levels
118
+ }, mode)));
119
+ server.registerTool("wity_ask", {
120
+ title: "Several questions in one call",
121
+ description: `Ask Wity several named questions about one text in one call. Each question is {type: "noul" | "choice" | "score", instructions, criteria}. criteria is {true, false} descriptions (optional) for noul, an object of option ids to descriptions for choice, and a list of levels (lowest first) for score. ${UNCALIBRATED}`,
122
+ inputSchema: {
123
+ text,
124
+ questions: z.record(z.string().min(1).max(64), questionSchema).describe("Questions by name. Answers come back under the same names."),
125
+ reasoning
126
+ },
127
+ annotations: readOnly
128
+ }, async ({ text: state, questions, reasoning: mode }) => {
129
+ if (Object.keys(questions).length === 0) return failed(new CliError("Give at least one question.", EXIT.usage));
130
+ return systemOne({
131
+ state,
132
+ questions,
133
+ ...mode && { reasoning: mode }
134
+ });
135
+ });
136
+ server.registerTool("wity_generate", {
137
+ title: "Write short text or JSON",
138
+ description: "Ask Wity to write short text from a text (up to 512 tokens), like a one-line reply or a value read off a document. With a JSON Schema shape, the output matches it. Check generated text before acting on it: it can say things that aren't in the input. If the answer is one of a known set, use wity_choice instead.",
139
+ inputSchema: {
140
+ instructions: z.string().min(1).describe("What to write, like 'The one-line reply to send to this customer'."),
141
+ text: text.optional(),
142
+ shape: z.record(z.string(), z.unknown()).optional().describe("A JSON Schema with \"type\": \"object\" at the top."),
143
+ max_tokens: z.number().int().min(1).max(512).optional().describe("The most tokens to write. Default 128.")
144
+ },
145
+ annotations: readOnly
146
+ }, async ({ instructions, text: state, shape, max_tokens }) => {
147
+ try {
148
+ guard();
149
+ if (shape && shape.type !== "object") throw new CliError("The shape needs \"type\": \"object\" at the top.", EXIT.usage);
150
+ const req = {
151
+ instructions,
152
+ ...state !== void 0 && { state },
153
+ ...shape && { shape },
154
+ ...max_tokens !== void 0 && { max_tokens }
155
+ };
156
+ const res = await client.generate(req, { signal: ctx.io.signal });
157
+ spentTokens += res.usage.input_tokens;
158
+ const cut = res.finish_reason === "length" ? " Cut off at max_tokens: raise it for the rest." : "";
159
+ return done(`Wrote ${res.usage.output_tokens} tokens · ${usage(res)}.${cut}`, res);
160
+ } catch (err) {
161
+ return failed(err instanceof CliError ? err : explain(err, found));
162
+ }
163
+ });
164
+ return server;
165
+ };
166
+ const serveMcp = async (ctx, opts) => {
167
+ const { io } = ctx;
168
+ if (opts.maxSpend !== void 0 && !(opts.maxSpend > 0)) throw new CliError("--max-spend is a dollar amount above 0.", EXIT.usage);
169
+ const found = await requireKey(ctx);
170
+ if (io.stdin.isTTY) io.stderr.write("wity mcp serve talks MCP over stdin and stdout, so an AI app should start it.\nFor example: claude mcp add wity -- wity mcp serve\nWaiting for MCP messages. Press Ctrl+C to stop.\n");
171
+ const server = createMcpServer(ctx, found, { maxSpendUsd: opts.maxSpend });
172
+ const transport = new StdioServerTransport(io.stdin, io.stdout);
173
+ await server.connect(transport);
174
+ await new Promise((resolve) => {
175
+ io.stdin.once("end", resolve);
176
+ io.stdin.once("close", resolve);
177
+ io.signal.addEventListener("abort", () => resolve(), { once: true });
178
+ });
179
+ await server.close();
180
+ return EXIT.ok;
181
+ };
182
+ //#endregion
183
+ export { serveMcp };
@@ -0,0 +1,4 @@
1
+ //#region package.json
2
+ var version = "0.1.0";
3
+ //#endregion
4
+ export { version as t };
@@ -0,0 +1,389 @@
1
+ import { C as cost, E as painter, F as CliError, I as EXIT, O as shouldColor, S as cleanBlock, _ as explain, a as checkStateLength, p as keyPrefix, t as requireKey, v as makeClient, x as clean } from "./send-CGXuVi9Z.mjs";
2
+ import { a as scoreQuestion, f as systemOneTs, n as choiceQuestion, o as renderAnswer, r as noulQuestion, s as renderFooter } from "./questions-eZ01WB1q.mjs";
3
+ import { Box, Text, render, useAnimation, useApp, useInput, usePaste, useWindowSize } from "ink";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/playground.tsx
7
+ const TYPES = [
8
+ "noul",
9
+ "choice",
10
+ "score"
11
+ ];
12
+ const REASONINGS = [
13
+ "off",
14
+ "auto",
15
+ "always"
16
+ ];
17
+ const LABEL_WIDTH = 13;
18
+ const FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
19
+ const fieldsFor = (type) => type === "noul" ? [
20
+ "text",
21
+ "type",
22
+ "question",
23
+ "reasoning"
24
+ ] : [
25
+ "text",
26
+ "type",
27
+ "question",
28
+ "options",
29
+ "reasoning"
30
+ ];
31
+ const cycle = (values, value, step) => values[(values.indexOf(value) + step + values.length) % values.length];
32
+ /** "billing=Payments; technical=Bugs" → ["billing=Payments", "technical=Bugs"] */
33
+ const splitOptions = (value) => value.split(";").map((part) => part.trim()).filter(Boolean);
34
+ /**
35
+ * A one-line text box. Ctrl combinations, Tab and arrows up and down are left to the playground.
36
+ * Pasted text arrives whole (bracketed paste), so a pasted line break never sends the question.
37
+ */
38
+ function Input(props) {
39
+ const { value, onChange, focus, placeholder, multiline } = props;
40
+ const [cursor, setCursor] = useState(value.length);
41
+ useEffect(() => setCursor((at) => Math.min(at, value.length)), [value]);
42
+ const insert = (text) => {
43
+ onChange(value.slice(0, cursor) + text + value.slice(cursor));
44
+ setCursor(cursor + text.length);
45
+ };
46
+ useInput((input, key) => {
47
+ if (key.ctrl || key.meta || key.tab || key.upArrow || key.downArrow || key.return || key.escape) return;
48
+ if (key.leftArrow) setCursor(Math.max(0, cursor - 1));
49
+ else if (key.rightArrow) setCursor(Math.min(value.length, cursor + 1));
50
+ else if (key.backspace || key.delete) {
51
+ if (cursor > 0) {
52
+ onChange(value.slice(0, cursor - 1) + value.slice(cursor));
53
+ setCursor(cursor - 1);
54
+ }
55
+ } else if (input) insert(cleanBlock(input).replaceAll("\n", ""));
56
+ }, { isActive: focus });
57
+ usePaste((pasted) => insert(multiline ? cleanBlock(pasted) : cleanBlock(pasted).replace(/\s*\n\s*/g, " ")), { isActive: focus });
58
+ const shown = value.replaceAll("\n", "⏎");
59
+ if (!focus) return shown ? /* @__PURE__ */ jsx(Text, { children: shown }) : /* @__PURE__ */ jsx(Text, {
60
+ dimColor: true,
61
+ children: placeholder
62
+ });
63
+ if (!shown) return /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
64
+ inverse: true,
65
+ children: " "
66
+ }), /* @__PURE__ */ jsx(Text, {
67
+ dimColor: true,
68
+ children: placeholder
69
+ })] });
70
+ return /* @__PURE__ */ jsxs(Text, { children: [
71
+ shown.slice(0, cursor),
72
+ /* @__PURE__ */ jsx(Text, {
73
+ inverse: true,
74
+ children: shown[cursor] ?? " "
75
+ }),
76
+ shown.slice(cursor + 1)
77
+ ] });
78
+ }
79
+ function Row(props) {
80
+ return /* @__PURE__ */ jsxs(Box, {
81
+ flexDirection: "column",
82
+ children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Box, {
83
+ width: LABEL_WIDTH,
84
+ flexShrink: 0,
85
+ children: /* @__PURE__ */ jsxs(Text, {
86
+ color: props.focus ? "cyan" : void 0,
87
+ dimColor: !props.focus,
88
+ children: [props.focus ? "› " : " ", props.label]
89
+ })
90
+ }), /* @__PURE__ */ jsx(Box, {
91
+ flexGrow: 1,
92
+ children: props.children
93
+ })] }), props.focus && props.hint ? /* @__PURE__ */ jsx(Box, {
94
+ marginLeft: LABEL_WIDTH,
95
+ children: /* @__PURE__ */ jsx(Text, {
96
+ dimColor: true,
97
+ children: props.hint
98
+ })
99
+ }) : null]
100
+ });
101
+ }
102
+ function Choices(props) {
103
+ return /* @__PURE__ */ jsx(Text, { children: props.values.map((value) => value === props.value ? /* @__PURE__ */ jsx(Text, {
104
+ color: "cyan",
105
+ bold: true,
106
+ inverse: props.focus,
107
+ children: ` ${value} `
108
+ }, value) : /* @__PURE__ */ jsx(Text, {
109
+ dimColor: true,
110
+ children: ` ${value} `
111
+ }, value)) });
112
+ }
113
+ function Working({ started }) {
114
+ const { frame } = useAnimation({ interval: 80 });
115
+ const slow = Date.now() - started > 1500;
116
+ return /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
117
+ color: "cyan",
118
+ children: FRAMES[frame % 10]
119
+ }), /* @__PURE__ */ jsx(Text, {
120
+ dimColor: true,
121
+ children: slow ? " Still working. Wity may be thinking this one through…" : " Asking Wity…"
122
+ })] });
123
+ }
124
+ function Playground(props) {
125
+ const { ctx, found, paint } = props;
126
+ const { exit } = useApp();
127
+ const { columns } = useWindowSize();
128
+ const [text, setText] = useState(props.initialText);
129
+ const [type, setType] = useState("noul");
130
+ const [question, setQuestion] = useState("");
131
+ const [options, setOptions] = useState("");
132
+ const [reasoning, setReasoning] = useState("auto");
133
+ const [focus, setFocus] = useState(props.initialText ? "question" : "text");
134
+ const [showCode, setShowCode] = useState(false);
135
+ const [status, setStatus] = useState({ kind: "idle" });
136
+ const [spent, setSpent] = useState({
137
+ calls: 0,
138
+ tokens: 0
139
+ });
140
+ const inFlight = useRef(null);
141
+ const fields = fieldsFor(type);
142
+ const move = (step) => setFocus((at) => cycle(fields, fields.includes(at) ? at : "type", step));
143
+ /** The request as it stands. Throws a CliError that says what's missing. */
144
+ const build = () => {
145
+ if (text.trim() === "") throw new CliError("Type or paste the text to judge first.", EXIT.usage);
146
+ checkStateLength(text);
147
+ const built = type === "noul" ? noulQuestion(question, void 0, void 0) : type === "choice" ? choiceQuestion(question, splitOptions(options)) : scoreQuestion(question, splitOptions(options));
148
+ return {
149
+ state: text,
150
+ questions: { answer: built },
151
+ reasoning
152
+ };
153
+ };
154
+ const ask = async () => {
155
+ if (inFlight.current) return;
156
+ let request;
157
+ try {
158
+ request = build();
159
+ } catch (err) {
160
+ const e = err;
161
+ setStatus({
162
+ kind: "error",
163
+ message: e.message,
164
+ hint: e.hint
165
+ });
166
+ return;
167
+ }
168
+ setShowCode(false);
169
+ const controller = new AbortController();
170
+ inFlight.current = controller;
171
+ setStatus({
172
+ kind: "running",
173
+ started: Date.now()
174
+ });
175
+ const quietIo = {
176
+ ...ctx.io,
177
+ stderr: { write: () => true }
178
+ };
179
+ const client = makeClient(found.key, ctx.baseURL, quietIo);
180
+ const started = performance.now();
181
+ try {
182
+ const res = await client.systemOne(request, { signal: AbortSignal.any([controller.signal, ctx.io.signal]) });
183
+ setSpent((s) => ({
184
+ calls: s.calls + 1,
185
+ tokens: s.tokens + res.usage.input_tokens
186
+ }));
187
+ setStatus({
188
+ kind: "done",
189
+ res,
190
+ totalMs: performance.now() - started,
191
+ instructions: question
192
+ });
193
+ } catch (err) {
194
+ const e = explain(err, found);
195
+ setStatus(e instanceof CliError ? {
196
+ kind: "error",
197
+ message: e.message,
198
+ hint: e.hint
199
+ } : {
200
+ kind: "error",
201
+ message: String(e)
202
+ });
203
+ } finally {
204
+ inFlight.current = null;
205
+ }
206
+ };
207
+ useInput((input, key) => {
208
+ if (key.ctrl && input === "c") {
209
+ if (inFlight.current) inFlight.current.abort();
210
+ else exit();
211
+ } else if (key.escape) {
212
+ if (showCode) setShowCode(false);
213
+ else exit();
214
+ } else if (key.ctrl && input === "t") setShowCode((shown) => !shown);
215
+ else if (key.tab) move(key.shift ? -1 : 1);
216
+ else if (key.downArrow) move(1);
217
+ else if (key.upArrow) move(-1);
218
+ else if (key.return) ask();
219
+ else if ((key.leftArrow || key.rightArrow) && (focus === "type" || focus === "reasoning")) {
220
+ const step = key.leftArrow ? -1 : 1;
221
+ if (focus === "type") setType((at) => cycle(TYPES, at, step));
222
+ else setReasoning((at) => cycle(REASONINGS, at, step));
223
+ }
224
+ });
225
+ let code;
226
+ if (showCode) try {
227
+ code = systemOneTs(build(), ctx.baseURL);
228
+ } catch (err) {
229
+ code = `// ${err.message}`;
230
+ }
231
+ const width = Math.min(columns, 100);
232
+ const optionsHint = type === "choice" ? "id=description; id=description (2 to 256 options, separated by ;)" : "lowest; …; highest (2 to 10 levels, separated by ;)";
233
+ return /* @__PURE__ */ jsxs(Box, {
234
+ flexDirection: "column",
235
+ width,
236
+ paddingX: 1,
237
+ children: [
238
+ /* @__PURE__ */ jsxs(Box, {
239
+ borderStyle: "round",
240
+ borderColor: "cyan",
241
+ paddingX: 1,
242
+ justifyContent: "space-between",
243
+ children: [/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
244
+ color: "cyan",
245
+ children: "◆ "
246
+ }), /* @__PURE__ */ jsx(Text, {
247
+ bold: true,
248
+ children: "wity playground"
249
+ })] }), /* @__PURE__ */ jsxs(Text, {
250
+ dimColor: true,
251
+ children: [
252
+ keyPrefix(found.key),
253
+ " · ",
254
+ found.source === "env" ? "WITY_API_KEY" : found.source,
255
+ spent.calls > 0 ? ` · ${spent.calls} call${spent.calls > 1 ? "s" : ""} ≈ ${cost(spent.tokens)}` : ""
256
+ ]
257
+ })]
258
+ }),
259
+ /* @__PURE__ */ jsxs(Box, {
260
+ flexDirection: "column",
261
+ marginTop: 1,
262
+ children: [
263
+ /* @__PURE__ */ jsx(Row, {
264
+ label: "Text",
265
+ focus: focus === "text",
266
+ hint: "what Wity reads · paste works, line breaks show as ⏎",
267
+ children: /* @__PURE__ */ jsx(Input, {
268
+ value: text,
269
+ onChange: setText,
270
+ focus: focus === "text",
271
+ placeholder: "My card was charged twice…",
272
+ multiline: true
273
+ })
274
+ }),
275
+ /* @__PURE__ */ jsx(Row, {
276
+ label: "Type",
277
+ focus: focus === "type",
278
+ hint: "← → to change",
279
+ children: /* @__PURE__ */ jsx(Choices, {
280
+ values: TYPES,
281
+ value: type,
282
+ focus: focus === "type"
283
+ })
284
+ }),
285
+ /* @__PURE__ */ jsx(Row, {
286
+ label: "Question",
287
+ focus: focus === "question",
288
+ children: /* @__PURE__ */ jsx(Input, {
289
+ value: question,
290
+ onChange: setQuestion,
291
+ focus: focus === "question",
292
+ placeholder: type === "noul" ? "Does the customer need this soon?" : "Which team should handle this?"
293
+ })
294
+ }),
295
+ type !== "noul" ? /* @__PURE__ */ jsx(Row, {
296
+ label: type === "choice" ? "Options" : "Levels",
297
+ focus: focus === "options",
298
+ hint: optionsHint,
299
+ children: /* @__PURE__ */ jsx(Input, {
300
+ value: options,
301
+ onChange: setOptions,
302
+ focus: focus === "options",
303
+ placeholder: type === "choice" ? "billing=Payments; technical=Bugs; other" : "Calm; Annoyed; Angry"
304
+ })
305
+ }) : null,
306
+ /* @__PURE__ */ jsx(Row, {
307
+ label: "Reasoning",
308
+ focus: focus === "reasoning",
309
+ hint: "← → to change · auto thinks only when needed",
310
+ children: /* @__PURE__ */ jsx(Choices, {
311
+ values: REASONINGS,
312
+ value: reasoning,
313
+ focus: focus === "reasoning"
314
+ })
315
+ })
316
+ ]
317
+ }),
318
+ /* @__PURE__ */ jsx(Box, {
319
+ marginTop: 1,
320
+ children: /* @__PURE__ */ jsxs(Text, {
321
+ dimColor: true,
322
+ children: [
323
+ "⏎ ask · tab next · ctrl+t ",
324
+ showCode ? "hide" : "show",
325
+ " code · esc quit"
326
+ ]
327
+ })
328
+ }),
329
+ /* @__PURE__ */ jsx(Box, {
330
+ flexDirection: "column",
331
+ marginTop: 1,
332
+ children: code !== void 0 ? /* @__PURE__ */ jsxs(Box, {
333
+ borderStyle: "round",
334
+ borderDimColor: true,
335
+ paddingX: 1,
336
+ flexDirection: "column",
337
+ children: [/* @__PURE__ */ jsx(Text, {
338
+ dimColor: true,
339
+ children: "TypeScript · npm install wity-sdk"
340
+ }), /* @__PURE__ */ jsx(Text, { children: code })]
341
+ }) : status.kind === "running" ? /* @__PURE__ */ jsx(Working, { started: status.started }) : status.kind === "error" ? /* @__PURE__ */ jsxs(Box, {
342
+ flexDirection: "column",
343
+ children: [/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
344
+ color: "red",
345
+ children: "✗ "
346
+ }), status.message] }), status.hint ? /* @__PURE__ */ jsx(Text, {
347
+ dimColor: true,
348
+ children: ` ${status.hint}`
349
+ }) : null]
350
+ }) : status.kind === "done" ? /* @__PURE__ */ jsxs(Box, {
351
+ flexDirection: "column",
352
+ children: [
353
+ /* @__PURE__ */ jsx(Text, {
354
+ bold: true,
355
+ children: clean(status.instructions)
356
+ }),
357
+ status.res.answers.answer ? /* @__PURE__ */ jsx(Text, { children: [
358
+ "",
359
+ ...renderAnswer(status.res.answers.answer, paint, width),
360
+ ""
361
+ ].join("\n") }) : null,
362
+ /* @__PURE__ */ jsx(Text, { children: renderFooter(status.res, status.totalMs, paint) })
363
+ ]
364
+ }) : null
365
+ })
366
+ ]
367
+ });
368
+ }
369
+ const playground = async (ctx, initialText) => {
370
+ const { io } = ctx;
371
+ if (!io.stdin.isTTY || !io.stdout.isTTY) throw new CliError("The playground needs a terminal.", EXIT.usage, "In scripts, use noul, choice, score or ask.");
372
+ const found = await requireKey(ctx);
373
+ const paint = painter(shouldColor(io.env, io.stdout));
374
+ await render(/* @__PURE__ */ jsx(Playground, {
375
+ ctx,
376
+ found,
377
+ initialText,
378
+ paint
379
+ }), {
380
+ stdin: io.stdin,
381
+ stdout: io.stdout,
382
+ stderr: io.stderr,
383
+ exitOnCtrlC: false,
384
+ patchConsole: false
385
+ }).waitUntilExit();
386
+ return EXIT.ok;
387
+ };
388
+ //#endregion
389
+ export { playground };