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/LICENSE +55 -0
- package/README.md +186 -0
- package/dist/cli.mjs +509 -0
- package/dist/mcp-DnqXX8ba.mjs +183 -0
- package/dist/package-7FOUqcPO.mjs +4 -0
- package/dist/playground-Cn7EIrd7.mjs +389 -0
- package/dist/questions-eZ01WB1q.mjs +325 -0
- package/dist/send-CGXuVi9Z.mjs +1052 -0
- package/package.json +50 -0
|
@@ -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,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 };
|