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,325 @@
|
|
|
1
|
+
import { A as truncate, B as score, C as cost, D as pct, F as CliError, I as EXIT, R as choice, T as ms, b as bar, j as writeJson, r as sendSystemOne, u as readState, w as decimal, x as clean, z as noul } from "./send-CGXuVi9Z.mjs";
|
|
2
|
+
//#region src/code.ts
|
|
3
|
+
/** A string as a JavaScript literal. JSON's escaping is valid JavaScript, and safe for any text. */
|
|
4
|
+
const str = (value) => JSON.stringify(value);
|
|
5
|
+
/** An object key: bare when it's a plain identifier, quoted otherwise. */
|
|
6
|
+
const prop = (key) => /^[A-Za-z_$][\w$]*$/.test(key) ? key : str(key);
|
|
7
|
+
/** A JSON value as JavaScript, indented to sit at `indent`. */
|
|
8
|
+
const literal = (value, indent) => JSON.stringify(value, null, 2).split("\n").map((line, i) => i === 0 ? line : indent + line).join("\n").replace(/^(\s*)"([A-Za-z_$][\w$]*)":/gm, "$1$2:");
|
|
9
|
+
const builder = (question, indent) => {
|
|
10
|
+
if (question.type === "noul") return question.criteria ? `noul(${str(question.instructions)}, ${literal(question.criteria, indent)})` : `noul(${str(question.instructions)})`;
|
|
11
|
+
if (question.type === "choice") {
|
|
12
|
+
const options = Object.entries(question.criteria).map(([id, description]) => `${indent} ${prop(id)}: ${str(description)},`).join("\n");
|
|
13
|
+
return `choice(${str(question.instructions)}, {\n${options}\n${indent}})`;
|
|
14
|
+
}
|
|
15
|
+
const levels = question.criteria.map(str);
|
|
16
|
+
const inline = `[${levels.join(", ")}]`;
|
|
17
|
+
return inline.length <= 80 ? `score(${str(question.instructions)}, ${inline})` : `score(${str(question.instructions)}, [\n${levels.map((level) => `${indent} ${level},`).join("\n")}\n${indent}])`;
|
|
18
|
+
};
|
|
19
|
+
const clientLine = (baseURL) => baseURL === "https://wity-proxy-production-2c33.up.railway.app" ? "const client = new WityClient(); // reads WITY_API_KEY" : `const client = new WityClient({ baseURL: ${str(baseURL)} }); // reads WITY_API_KEY`;
|
|
20
|
+
const systemOneTs = (request, baseURL) => {
|
|
21
|
+
const types = [...new Set(Object.values(request.questions).map((q) => q.type))].sort();
|
|
22
|
+
const questions = Object.entries(request.questions).map(([name, question]) => ` ${prop(name)}: ${builder(question, " ")},`).join("\n");
|
|
23
|
+
const extras = [request.reasoning && ` reasoning: ${str(request.reasoning)},`, request.max_latency_ms !== void 0 && ` max_latency_ms: ${request.max_latency_ms},`].filter(Boolean);
|
|
24
|
+
const first = Object.entries(request.questions)[0];
|
|
25
|
+
const field = first?.[1].type === "noul" ? "noul" : first?.[1].type === "choice" ? "choice" : "score";
|
|
26
|
+
return [
|
|
27
|
+
`import { WityClient, ${types.join(", ")} } from "wity-sdk";`,
|
|
28
|
+
"",
|
|
29
|
+
clientLine(baseURL),
|
|
30
|
+
"",
|
|
31
|
+
"const res = await client.systemOne({",
|
|
32
|
+
` state: ${literal(request.state, " ")},`,
|
|
33
|
+
" questions: {",
|
|
34
|
+
questions,
|
|
35
|
+
" },",
|
|
36
|
+
...extras,
|
|
37
|
+
"});",
|
|
38
|
+
"",
|
|
39
|
+
first ? `console.log(res.answers.${prop(first[0])}.${field});` : "console.log(res.answers);"
|
|
40
|
+
].join("\n");
|
|
41
|
+
};
|
|
42
|
+
const generateTs = (request, baseURL) => {
|
|
43
|
+
const fields = [
|
|
44
|
+
request.state !== void 0 && ` state: ${literal(request.state, " ")},`,
|
|
45
|
+
` instructions: ${str(request.instructions)},`,
|
|
46
|
+
request.shape !== void 0 && ` shape: ${literal(request.shape, " ")},`,
|
|
47
|
+
request.max_tokens !== void 0 && ` max_tokens: ${request.max_tokens},`
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
return [
|
|
50
|
+
`import { WityClient } from "wity-sdk";`,
|
|
51
|
+
"",
|
|
52
|
+
clientLine(baseURL),
|
|
53
|
+
"",
|
|
54
|
+
"const res = await client.generate({",
|
|
55
|
+
...fields,
|
|
56
|
+
"});",
|
|
57
|
+
"",
|
|
58
|
+
request.shape !== void 0 ? "console.log(res.value);" : "console.log(res.text);"
|
|
59
|
+
].join("\n");
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A curl command. The body goes in a quoted heredoc, so the shell never expands anything in it.
|
|
63
|
+
* JSON strings can't hold a raw line break, so no line of the body can end the heredoc early.
|
|
64
|
+
*/
|
|
65
|
+
const curl = (route, body, baseURL) => [
|
|
66
|
+
`curl ${baseURL}${route} \\`,
|
|
67
|
+
` -H "Authorization: Bearer $WITY_API_KEY" \\`,
|
|
68
|
+
` -H "Content-Type: application/json" \\`,
|
|
69
|
+
" -d @- <<'JSON'",
|
|
70
|
+
JSON.stringify(body, null, 2),
|
|
71
|
+
"JSON"
|
|
72
|
+
].join("\n");
|
|
73
|
+
/** `--code`, `--code ts` or `--code curl`. Commander gives `true` for the bare flag. */
|
|
74
|
+
const codeLangs = (value) => {
|
|
75
|
+
if (value === void 0 || value === false) return void 0;
|
|
76
|
+
if (value === true) return ["ts", "curl"];
|
|
77
|
+
if (value === "ts" || value === "curl") return [value];
|
|
78
|
+
throw new CliError("--code takes ts or curl, or nothing for both.", EXIT.usage);
|
|
79
|
+
};
|
|
80
|
+
const printCode = (ctx, langs, ts, sh) => {
|
|
81
|
+
const { io } = ctx;
|
|
82
|
+
const pretty = Boolean(io.stdout.isTTY);
|
|
83
|
+
const show = (title, code) => {
|
|
84
|
+
if (pretty) io.stdout.write(`\n ${ctx.out("dim", title)}\n\n`);
|
|
85
|
+
io.stdout.write(`${code}\n`);
|
|
86
|
+
};
|
|
87
|
+
if (langs.includes("ts")) show("TypeScript · npm install wity-sdk", ts);
|
|
88
|
+
if (langs.includes("curl")) show("curl", sh);
|
|
89
|
+
if (pretty) io.stdout.write(`\n ${ctx.out("dim", "Nothing was sent. Set WITY_API_KEY before running this.")}\n\n`);
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/render.ts
|
|
93
|
+
/** Choice answers with more options than this show the top ones, and say how many are hidden. */
|
|
94
|
+
const MAX_ROWS = 10;
|
|
95
|
+
const MAX_LABEL = 28;
|
|
96
|
+
const barWidth = (columns) => columns !== void 0 && columns < 72 ? 14 : 28;
|
|
97
|
+
const rows = (items, paint, columns) => {
|
|
98
|
+
const width = barWidth(columns);
|
|
99
|
+
const labelWidth = Math.max(...items.map((row) => row.label.length));
|
|
100
|
+
const tagWidth = Math.max(0, ...items.map((row) => row.tag?.length ?? 0));
|
|
101
|
+
return items.map((row) => {
|
|
102
|
+
const mark = row.winner ? paint("green", "●") : " ";
|
|
103
|
+
const tag = row.tag !== void 0 ? `${paint("dim", row.tag.padStart(tagWidth))} ` : "";
|
|
104
|
+
const label = row.label.padEnd(labelWidth);
|
|
105
|
+
const value = pct(row.p).padStart(7);
|
|
106
|
+
return ` ${mark} ${tag}${row.winner ? paint("bold", label) : label} ${bar(row.p, width, paint, row.winner ? "green" : "gray")} ${row.winner ? paint("bold", value) : paint("dim", value)}`;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const REASONS = {
|
|
110
|
+
close_call: "the top options were close",
|
|
111
|
+
order_sensitive: "the answer changed when the options were reordered",
|
|
112
|
+
forecast: "the question asks for a likelihood",
|
|
113
|
+
requested: "reasoning was set to always"
|
|
114
|
+
};
|
|
115
|
+
/** A note when Wity thought before answering, with the answer it had before thinking. */
|
|
116
|
+
const thinking = (info, before, paint) => {
|
|
117
|
+
const lines = [];
|
|
118
|
+
if (info?.thought) {
|
|
119
|
+
const why = info.reason ? REASONS[info.reason] ?? clean(info.reason).replaceAll("_", " ") : void 0;
|
|
120
|
+
const tokens = info.thought_tokens ? ` ${info.thought_tokens.toLocaleString("en")} thought tokens.` : "";
|
|
121
|
+
lines.push(` ${paint("magenta", "◆")} ${paint("dim", `Wity thought first${why ? `, because ${why}` : ""}.${tokens}`)}`);
|
|
122
|
+
if (before) lines.push(` ${paint("dim", `Before thinking: ${before}`)}`);
|
|
123
|
+
}
|
|
124
|
+
if (info?.budget_limited) lines.push(` ${paint("yellow", "▲ Thinking was cut short to meet --max-latency.")}`);
|
|
125
|
+
return lines;
|
|
126
|
+
};
|
|
127
|
+
const topOf = (probabilities) => Object.entries(probabilities).reduce((best, entry) => entry[1] > best[1] ? entry : best, ["", -1])[0];
|
|
128
|
+
const renderNoul = (answer, paint, columns) => {
|
|
129
|
+
const yes = answer.noul;
|
|
130
|
+
const lines = rows([{
|
|
131
|
+
label: "yes",
|
|
132
|
+
p: yes,
|
|
133
|
+
winner: yes >= .5
|
|
134
|
+
}, {
|
|
135
|
+
label: "no",
|
|
136
|
+
p: 1 - yes,
|
|
137
|
+
winner: yes < .5
|
|
138
|
+
}], paint, columns);
|
|
139
|
+
const before = answer.direct_noul !== void 0 ? `yes ${pct(answer.direct_noul)}` : void 0;
|
|
140
|
+
return [...lines, ...spaced(thinking(answer.reasoning, before, paint))];
|
|
141
|
+
};
|
|
142
|
+
const renderChoice = (answer, paint, columns) => {
|
|
143
|
+
const sorted = Object.entries(answer.probabilities).sort((a, b) => b[1] - a[1]);
|
|
144
|
+
const shown = sorted.slice(0, MAX_ROWS);
|
|
145
|
+
const lines = rows(shown.map(([id, p]) => ({
|
|
146
|
+
label: truncate(clean(id), MAX_LABEL),
|
|
147
|
+
p,
|
|
148
|
+
winner: id === answer.choice
|
|
149
|
+
})), paint, columns);
|
|
150
|
+
if (sorted.length > shown.length) lines.push(` ${paint("dim", `and ${sorted.length - shown.length} more options. Use --json to see all.`)}`);
|
|
151
|
+
lines.push("", ` ${paint("dim", `confidence ${decimal(answer.confidence)}`)}`);
|
|
152
|
+
const direct = answer.direct_probabilities;
|
|
153
|
+
const top = direct && topOf(direct);
|
|
154
|
+
const before = direct && top ? `${truncate(clean(top), MAX_LABEL)} ${pct(direct[top] ?? 0)}` : void 0;
|
|
155
|
+
return [...lines, ...spaced(thinking(answer.reasoning, before, paint))];
|
|
156
|
+
};
|
|
157
|
+
const renderScore = (answer, paint, columns) => {
|
|
158
|
+
const levels = Object.keys(answer.legend).sort((a, b) => Number(a) - Number(b));
|
|
159
|
+
const top = topOf(answer.probabilities);
|
|
160
|
+
const name = (level) => truncate(clean(answer.legend[level ?? ""] ?? ""), MAX_LABEL);
|
|
161
|
+
const low = levels[0];
|
|
162
|
+
const high = levels[levels.length - 1];
|
|
163
|
+
const head = ` ${paint("bold", answer.score.toFixed(2))} ${paint("dim", `on a scale from ${low} (${name(low)}) to ${high} (${name(high)})`)}`;
|
|
164
|
+
const lines = rows(levels.map((level) => ({
|
|
165
|
+
tag: level,
|
|
166
|
+
label: name(level),
|
|
167
|
+
p: answer.probabilities[level] ?? 0,
|
|
168
|
+
winner: level === top
|
|
169
|
+
})), paint, columns);
|
|
170
|
+
const direct = answer.direct_probabilities;
|
|
171
|
+
const before = direct ? `score ${Object.entries(direct).reduce((sum, [level, p]) => sum + Number(level) * p, 0).toFixed(2)}` : void 0;
|
|
172
|
+
return [
|
|
173
|
+
head,
|
|
174
|
+
"",
|
|
175
|
+
...lines,
|
|
176
|
+
"",
|
|
177
|
+
` ${paint("dim", `confidence ${decimal(answer.confidence)}`)}`,
|
|
178
|
+
...spaced(thinking(answer.reasoning, before, paint))
|
|
179
|
+
];
|
|
180
|
+
};
|
|
181
|
+
/** Any answer, by its type. */
|
|
182
|
+
const renderAnswer = (answer, paint, columns) => answer.type === "noul" ? renderNoul(answer, paint, columns) : answer.type === "choice" ? renderChoice(answer, paint, columns) : renderScore(answer, paint, columns);
|
|
183
|
+
/** A blank line before a block, if there is a block. */
|
|
184
|
+
const spaced = (lines) => lines.length > 0 ? ["", ...lines] : [];
|
|
185
|
+
/** The last line: model, time on Wity, total time, tokens and estimated cost. */
|
|
186
|
+
const renderFooter = (res, totalMs, paint) => {
|
|
187
|
+
return ` ${paint("dim", [
|
|
188
|
+
clean(res.model),
|
|
189
|
+
`${ms(res.metadata.elapsed_ms)} on Wity`,
|
|
190
|
+
`${ms(totalMs)} total`,
|
|
191
|
+
`${res.usage.input_tokens.toLocaleString("en")} input tokens`,
|
|
192
|
+
`≈ ${cost(res.usage.input_tokens)}`
|
|
193
|
+
].join(" · "))}`;
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/commands/questions.ts
|
|
197
|
+
/** Answers come back under the question's name. A single question is always called this. */
|
|
198
|
+
const NAME = "answer";
|
|
199
|
+
const needText = (value, what) => {
|
|
200
|
+
if (value.trim() === "") throw new CliError(`The ${what} is empty.`, EXIT.usage);
|
|
201
|
+
return value;
|
|
202
|
+
};
|
|
203
|
+
/** `--option id=description`, or just `--option id` when the id says enough. */
|
|
204
|
+
const choiceQuestion = (instructions, options) => {
|
|
205
|
+
if (options.length < 2 || options.length > 256) throw new CliError(`A choice needs 2 to 256 options. You gave ${options.length}.`, EXIT.usage, "Add each with --option id=\"description\".");
|
|
206
|
+
const criteria = {};
|
|
207
|
+
for (const option of options) {
|
|
208
|
+
const at = option.indexOf("=");
|
|
209
|
+
const id = (at === -1 ? option : option.slice(0, at)).trim();
|
|
210
|
+
const description = (at === -1 ? option : option.slice(at + 1)).trim();
|
|
211
|
+
if (!id) throw new CliError(`The option "${clean(option)}" has no id before the =.`, EXIT.usage);
|
|
212
|
+
if (id in criteria) throw new CliError(`The option id "${clean(id)}" is used twice.`, EXIT.usage);
|
|
213
|
+
criteria[id] = description || id;
|
|
214
|
+
}
|
|
215
|
+
return choice(needText(instructions, "question"), criteria);
|
|
216
|
+
};
|
|
217
|
+
const noulQuestion = (instructions, yes, no) => {
|
|
218
|
+
if (yes === void 0 !== (no === void 0)) throw new CliError("Describe both answers, with --yes and --no, or neither.", EXIT.usage);
|
|
219
|
+
const text = needText(instructions, "question");
|
|
220
|
+
return yes !== void 0 && no !== void 0 ? noul(text, {
|
|
221
|
+
true: yes,
|
|
222
|
+
false: no
|
|
223
|
+
}) : noul(text);
|
|
224
|
+
};
|
|
225
|
+
const scoreQuestion = (instructions, levels) => {
|
|
226
|
+
if (levels.length < 2 || levels.length > 10) throw new CliError(`A score needs 2 to 10 levels. You gave ${levels.length}.`, EXIT.usage, "Add each with --level, lowest first.");
|
|
227
|
+
levels.forEach((level) => {
|
|
228
|
+
needText(level, "level");
|
|
229
|
+
});
|
|
230
|
+
return score(needText(instructions, "question"), levels);
|
|
231
|
+
};
|
|
232
|
+
/** Checked before the call, so a mistake costs nothing. */
|
|
233
|
+
const checkGate = (question, gate) => {
|
|
234
|
+
const { failUnder, expect } = gate;
|
|
235
|
+
if (expect !== void 0 && question.type === "choice" && !(expect in question.criteria)) throw new CliError(`--expect "${clean(expect)}" isn't one of the options.`, EXIT.usage);
|
|
236
|
+
if (failUnder === void 0) return;
|
|
237
|
+
if (question.type === "score") {
|
|
238
|
+
const top = question.criteria.length - 1;
|
|
239
|
+
if (failUnder < 0 || failUnder > top) throw new CliError(`For a score, --fail-under is a level from 0 to ${top}.`, EXIT.usage);
|
|
240
|
+
} else if (failUnder < 0 || failUnder > 1) throw new CliError("--fail-under is a probability from 0 to 1.", EXIT.usage);
|
|
241
|
+
};
|
|
242
|
+
/** `undefined` when it passes, or the reason it failed. */
|
|
243
|
+
const failedGate = (answer, gate) => {
|
|
244
|
+
const { failUnder, expect } = gate;
|
|
245
|
+
if (answer.type === "noul") {
|
|
246
|
+
if (failUnder !== void 0 && answer.noul < failUnder) return `yes is ${pct(answer.noul)}, under --fail-under ${failUnder}.`;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (answer.type === "score") {
|
|
250
|
+
if (failUnder !== void 0 && answer.score < failUnder) return `The score is ${answer.score.toFixed(2)}, under --fail-under ${failUnder}.`;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const option = expect ?? answer.choice;
|
|
254
|
+
const p = answer.probabilities[option] ?? 0;
|
|
255
|
+
if (failUnder !== void 0) return p < failUnder ? `${clean(option)} is ${pct(p)}, under --fail-under ${failUnder}.` : void 0;
|
|
256
|
+
if (expect !== void 0 && answer.choice !== expect) return `Wity chose ${clean(answer.choice)}, not ${clean(expect)}.`;
|
|
257
|
+
};
|
|
258
|
+
/** A value inside the answer, by dotted path: `noul`, `choice`, `probabilities.billing`, `reasoning.thought`. */
|
|
259
|
+
const pickField = (answer, path) => {
|
|
260
|
+
let value = answer;
|
|
261
|
+
for (const part of path.split(".")) {
|
|
262
|
+
if (typeof value !== "object" || value === null || !Object.hasOwn(value, part)) {
|
|
263
|
+
const fields = Object.keys(answer).join(", ");
|
|
264
|
+
throw new CliError(`The answer has no field "${clean(path)}".`, EXIT.usage, `Fields: ${fields}`);
|
|
265
|
+
}
|
|
266
|
+
value = value[part];
|
|
267
|
+
}
|
|
268
|
+
if (typeof value === "string") return clean(value);
|
|
269
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
270
|
+
return String(value);
|
|
271
|
+
};
|
|
272
|
+
const ask = async (ctx, question, flags, gate = {}) => {
|
|
273
|
+
const { io } = ctx;
|
|
274
|
+
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);
|
|
275
|
+
checkGate(question, {
|
|
276
|
+
failUnder: flags.failUnder,
|
|
277
|
+
...gate
|
|
278
|
+
});
|
|
279
|
+
const allGates = {
|
|
280
|
+
failUnder: flags.failUnder,
|
|
281
|
+
...gate
|
|
282
|
+
};
|
|
283
|
+
const request = {
|
|
284
|
+
state: await readState(flags, io),
|
|
285
|
+
questions: { [NAME]: question },
|
|
286
|
+
...flags.reasoning && { reasoning: flags.reasoning },
|
|
287
|
+
...flags.maxLatency !== void 0 && { max_latency_ms: flags.maxLatency }
|
|
288
|
+
};
|
|
289
|
+
if (flags.dryRun) {
|
|
290
|
+
writeJson(io, request);
|
|
291
|
+
if (io.stderr.isTTY) io.stderr.write(` ${ctx.err("dim", "Dry run: nothing was sent.")}\n`);
|
|
292
|
+
return EXIT.ok;
|
|
293
|
+
}
|
|
294
|
+
const langs = codeLangs(flags.code);
|
|
295
|
+
if (langs) {
|
|
296
|
+
printCode(ctx, langs, systemOneTs(request, ctx.baseURL), curl("/v1/systemone", request, ctx.baseURL));
|
|
297
|
+
return EXIT.ok;
|
|
298
|
+
}
|
|
299
|
+
const { res, totalMs } = await sendSystemOne(ctx, request);
|
|
300
|
+
const answer = res.answers[NAME];
|
|
301
|
+
if (!answer) throw new CliError("Wity's answer was missing the question. Try again.", EXIT.error);
|
|
302
|
+
if (flags.field !== void 0) io.stdout.write(`${pickField(answer, flags.field)}\n`);
|
|
303
|
+
else if (flags.json || !io.stdout.isTTY) writeJson(io, res);
|
|
304
|
+
else {
|
|
305
|
+
const p = ctx.out;
|
|
306
|
+
const lines = [
|
|
307
|
+
"",
|
|
308
|
+
` ${p("bold", clean(question.instructions))}`,
|
|
309
|
+
"",
|
|
310
|
+
...renderAnswer(answer, p, io.stdout.columns),
|
|
311
|
+
"",
|
|
312
|
+
renderFooter(res, totalMs, p),
|
|
313
|
+
""
|
|
314
|
+
];
|
|
315
|
+
io.stdout.write(`${lines.join("\n")}\n`);
|
|
316
|
+
}
|
|
317
|
+
const failed = failedGate(answer, allGates);
|
|
318
|
+
if (failed) {
|
|
319
|
+
io.stderr.write(` ${ctx.err("red", "✗")} ${failed}\n`);
|
|
320
|
+
return EXIT.gate;
|
|
321
|
+
}
|
|
322
|
+
return EXIT.ok;
|
|
323
|
+
};
|
|
324
|
+
//#endregion
|
|
325
|
+
export { scoreQuestion as a, codeLangs as c, printCode as d, systemOneTs as f, pickField as i, curl as l, choiceQuestion as n, renderAnswer as o, noulQuestion as r, renderFooter as s, ask as t, generateTs as u };
|