@cloudraker/milliseconds 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.js ADDED
@@ -0,0 +1,1402 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { parseArgs } from "node:util";
4
+ import { readFileSync } from "node:fs";
5
+ //#region src/errors.ts
6
+ /** A brand property, so a duplicated copy of the package in a bundle still matches. */
7
+ const BRAND = "~milliseconds.error";
8
+ /**
9
+ * One error class. Switch on `code`: the union narrows exhaustively and never goes stale
10
+ * when the API adds a code.
11
+ */
12
+ var MillisecondsError = class extends Error {
13
+ name = "MillisecondsError";
14
+ [BRAND] = true;
15
+ code;
16
+ /** 0 when the call never reached the API. */
17
+ status;
18
+ /** The API's own message, unchanged. `message` adds one hint line. */
19
+ apiMessage;
20
+ /** Seconds from the retry-after header. Only 429 rate_limit_exceeded carries it. */
21
+ retryAfter;
22
+ rateLimit;
23
+ response;
24
+ /** Attempts this call made, including the first. */
25
+ attempts;
26
+ /** True for the codes the SDK retries. */
27
+ retryable;
28
+ constructor(init) {
29
+ super(init.message ?? init.apiMessage, init.cause === void 0 ? void 0 : { cause: init.cause });
30
+ this.code = init.code;
31
+ this.status = init.status ?? 0;
32
+ this.apiMessage = init.apiMessage;
33
+ this.retryAfter = init.retryAfter ?? null;
34
+ this.rateLimit = init.rateLimit ?? null;
35
+ this.response = init.response ?? null;
36
+ this.attempts = init.attempts ?? 1;
37
+ this.retryable = init.retryable ?? false;
38
+ }
39
+ };
40
+ function isMillisecondsError(e) {
41
+ return typeof e === "object" && e !== null && e[BRAND] === true;
42
+ }
43
+ /** Every client-side check throws this: status 0, code client_error, nothing sent. */
44
+ const clientError = (message) => new MillisecondsError({
45
+ code: "client_error",
46
+ status: 0,
47
+ apiMessage: message
48
+ });
49
+ //#endregion
50
+ //#region src/validate.ts
51
+ const MAX_CHARS = 2e4;
52
+ const MAX_TEXTS = 32;
53
+ const MAX_ITEMS = 32;
54
+ const n = (x) => x.toLocaleString("en-US");
55
+ const count = (size) => size === 1 ? "1 entry" : `${n(size)} entries`;
56
+ /** The key is a secret, so a browser bundle needs an explicit opt-in. */
57
+ function checkRuntime(apiKey, allowBrowser) {
58
+ if (!apiKey) throw clientError("No API key. Pass new DecisionMachine({ apiKey }) or set MS_API_KEY. Get a key at https://console.milliseconds.ai.");
59
+ if (typeof globalThis === "object" && typeof globalThis.document?.createElement === "function" && !allowBrowser) throw clientError("The API key is a secret. Call the API from your server, or pass dangerouslyAllowBrowser: true when the bundle never reaches a user.");
60
+ }
61
+ /**
62
+ * `texts` is 1 to 32 items, and every text is 1 to 20,000 characters.
63
+ *
64
+ * An empty text is a 400, not the 200-with-empty-results trap. That trap needs `text` and
65
+ * `texts` both absent, and the SDK always sends one of them. This check only replaces a
66
+ * round trip with a local error.
67
+ */
68
+ function checkInput(input) {
69
+ if (typeof input === "string") {
70
+ if (input === "") throw empty("text");
71
+ if (input.length > MAX_CHARS) throw tooLong("text", input.length);
72
+ return;
73
+ }
74
+ if (input.length === 0) throw clientError("texts is empty. Send at least one text.");
75
+ if (input.length > MAX_TEXTS) throw clientError(`texts has ${n(input.length)} items. The limit is ${n(MAX_TEXTS)}. Split the batch.`);
76
+ for (const [i, text] of input.entries()) {
77
+ if (text === "") throw empty(`texts[${i}]`);
78
+ if (text.length > MAX_CHARS) throw tooLong(`texts[${i}]`, text.length);
79
+ }
80
+ }
81
+ const empty = (where) => clientError(`${where} is empty. Send at least one character.`);
82
+ const tooLong = (where, length) => clientError(`${where} is ${n(length)} characters. The limit is ${n(MAX_CHARS)}. Split on paragraphs and send the parts as texts.`);
83
+ /**
84
+ * `statements` and `questions` are 1 to 32 items. The wire message for a body without one
85
+ * is `provide statement or statements, not both`, which names both fields and misleads.
86
+ */
87
+ function checkSpec(name, value) {
88
+ if (typeof value === "string") {
89
+ if (value !== "") return;
90
+ if (name === "statements") throw clientError("yes-no needs a statement. The wire message for a body without one names both fields and misleads.");
91
+ throw clientError(`${name} has 0 items. Send 1 to ${MAX_ITEMS}.`);
92
+ }
93
+ if (value.length === 0 || value.length > MAX_ITEMS) throw clientError(`${name} has ${n(value.length)} items. Send 1 to ${MAX_ITEMS}.`);
94
+ }
95
+ /**
96
+ * `labels` is 2 to 64, `types` is 1 to 64 and `scale` is 2 to 10. Three limits, because
97
+ * `decide.schema.ts` holds three. One shared rule would reject a legal single-type
98
+ * `entities` call.
99
+ *
100
+ * `decide.schema.ts` bounds the array branch of `labels` and `types` only: the
101
+ * `name -> description` branch is a plain `z.record`, so any number of described labels is
102
+ * legal. `classify-tree` passes `bounded` because its own `superRefine` bounds every level.
103
+ */
104
+ function checkList(name, value, min, max, capability, bounded = Array.isArray(value)) {
105
+ const size = Array.isArray(value) ? value.length : Object.keys(value).length;
106
+ if (bounded ? size >= min && size <= max : size > 0) return;
107
+ const found = size === 0 ? `${name} is empty.` : `${name} has ${count(size)}.`;
108
+ throw clientError(`${found} ${capability} needs ${min} to ${max}.`);
109
+ }
110
+ /** `planFor` throws unless the schema is an object with properties. Beat it locally. */
111
+ function checkSchema(schema) {
112
+ const s = schema;
113
+ if ((Array.isArray(s?.type) ? s.type.find((t) => t !== "null") : s?.type) !== "object" || typeof s?.properties !== "object" || s.properties === null) throw clientError("The schema must be an object with properties.");
114
+ }
115
+ //#endregion
116
+ //#region src/client.ts
117
+ const USER_AGENT = `cloudraker-milliseconds-js/0.1.0`;
118
+ const DEFAULT_BASE_URL = "https://api.milliseconds.ai";
119
+ const RETRY = /* @__PURE__ */ new Set([
120
+ "rate_limit_exceeded",
121
+ "runner_error",
122
+ "overloaded",
123
+ "connection_error",
124
+ "timeout"
125
+ ]);
126
+ /** Full jitter, capped at 8 s. */
127
+ const backoff = (n) => Math.random() * Math.min(500 * 2 ** n, 8e3);
128
+ /** Resolves early when the caller aborts. The loop then throws the caller's reason. */
129
+ const sleep = (ms, signal) => new Promise((resolve) => {
130
+ const timer = setTimeout(resolve, ms);
131
+ signal?.addEventListener("abort", () => {
132
+ clearTimeout(timer);
133
+ resolve();
134
+ }, { once: true });
135
+ });
136
+ /** Reads an environment variable without assuming `process` exists. */
137
+ function env(name) {
138
+ try {
139
+ return globalThis.process?.env?.[name];
140
+ } catch {
141
+ return;
142
+ }
143
+ }
144
+ /** A Promise of the result, with the headers one call away. No Promise subclass. */
145
+ const decision = (p) => {
146
+ const result = p.then((r) => r.result);
147
+ return Object.assign(result, { withUsage: () => {
148
+ result.catch(() => {});
149
+ return p;
150
+ } });
151
+ };
152
+ function rateLimitOf(h) {
153
+ const limitRequests = h.get("x-ratelimit-limit-requests");
154
+ if (limitRequests === null) return null;
155
+ const num = (k) => Number(h.get(k)) || 0;
156
+ return {
157
+ limitRequests: Number(limitRequests) || 0,
158
+ remainingRequests: num("x-ratelimit-remaining-requests"),
159
+ resetRequests: h.get("x-ratelimit-reset-requests") ?? "",
160
+ limitTokens: num("x-ratelimit-limit-tokens"),
161
+ remainingTokens: num("x-ratelimit-remaining-tokens"),
162
+ resetTokens: h.get("x-ratelimit-reset-tokens") ?? ""
163
+ };
164
+ }
165
+ function usageOf(response) {
166
+ const h = response.headers;
167
+ const num = (k) => Number(h.get(k)) || 0;
168
+ return {
169
+ inputChars: num("x-input-chars"),
170
+ inputTokens: num("x-input-tokens"),
171
+ inferenceMs: num("x-inference-ms"),
172
+ rateLimit: rateLimitOf(h),
173
+ headers: h
174
+ };
175
+ }
176
+ /** The hint line under the API message. Section 8.2 of DESIGN.md holds the copy. */
177
+ function describe(code, status, apiMessage, attempts, ms) {
178
+ const over = attempts > 1 ? ` after ${attempts} attempts over ${(ms / 1e3).toFixed(1)}s` : "";
179
+ const tail = apiMessage ? `:\n${apiMessage}` : ".";
180
+ switch (code) {
181
+ case "missing_api_key":
182
+ case "invalid_api_key": return `milliseconds rejected the API key (${status} ${code}).\nKeys start with "sk-ms-". Check MS_API_KEY, or create a key at\nhttps://console.milliseconds.ai.`;
183
+ case "rate_limit_exceeded": return `rate limited (${status} ${code})${over}${tail}\n Lower your concurrency, or raise maxRetries. Limits are per organization and\n shared by every key.`;
184
+ case "insufficient_quota": return `no token credits left (${status} ${code})${tail}\n Not retried. A timer retry will not help.`;
185
+ case "overloaded": return `every inference slot stayed busy (${status} ${code})${over}${tail}\n This is backpressure, not a fault. Send fewer texts per call, or back off further.\n A texts batch of 32 asks for 32 slots at once.`;
186
+ case "runner_error": return `inference failed twice (${status} ${code})${over}${tail}\n The model, not your request. Retry later, or raise maxRetries.`;
187
+ case "timeout": return `the request timed out${over}${tail}\n Raise timeout, or send fewer texts per call.`;
188
+ case "connection_error": return `could not reach the API${over}${tail}\n Check baseUrl and the network. maxRetries: 0 fails fast.`;
189
+ default: return `milliseconds rejected the request (${status} ${code})${tail}${apiMessage.startsWith("labels:") ? "\n classify needs two or more labels. Describe each one. Described labels score\n measurably better than bare names." : ""}`;
190
+ }
191
+ }
192
+ /** One AbortSignal for the attempt: the per-attempt timeout, plus the caller's signal. */
193
+ function attemptSignal(timeout, signal) {
194
+ const hasAny = typeof AbortSignal.any === "function";
195
+ if (typeof AbortSignal.timeout === "function" && (!signal || hasAny)) {
196
+ const t = AbortSignal.timeout(timeout);
197
+ return {
198
+ signal: signal ? AbortSignal.any([signal, t]) : t,
199
+ done: () => {}
200
+ };
201
+ }
202
+ const controller = new AbortController();
203
+ const timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeout);
204
+ signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
205
+ return {
206
+ signal: controller.signal,
207
+ done: () => clearTimeout(timer)
208
+ };
209
+ }
210
+ /** Transport: fetch, retries, errors and usage. Every capability is two lines on top. */
211
+ var Client = class {
212
+ baseUrl;
213
+ #apiKey;
214
+ #timeout;
215
+ #maxRetries;
216
+ #headers;
217
+ #fetch;
218
+ constructor(options = {}) {
219
+ const apiKey = options.apiKey ?? env("MS_API_KEY");
220
+ checkRuntime(apiKey, options.dangerouslyAllowBrowser);
221
+ this.#apiKey = apiKey;
222
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
223
+ this.#timeout = options.timeout ?? 6e4;
224
+ this.#maxRetries = options.maxRetries ?? 2;
225
+ this.#headers = { ...options.headers };
226
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
227
+ }
228
+ /** Escape hatch. Your path, your body, your type, the SDK's auth, retries and errors. */
229
+ post(path, body, options) {
230
+ return this.call(path, body, options, (raw) => raw);
231
+ }
232
+ /** `unwrap` maps the parsed body to the result. It follows the request, never the response. */
233
+ call(path, body, options, unwrap) {
234
+ return decision(this.send(path, body, options ?? {}).then(({ response, raw }) => ({
235
+ result: unwrap(raw),
236
+ usage: usageOf(response),
237
+ response
238
+ })));
239
+ }
240
+ async send(path, body, call) {
241
+ const url = `${this.baseUrl}${path}`;
242
+ const maxRetries = call.maxRetries ?? this.#maxRetries;
243
+ const timeout = call.timeout ?? this.#timeout;
244
+ const headers = new Headers({
245
+ "content-type": "application/json",
246
+ "user-agent": USER_AGENT
247
+ });
248
+ for (const [k, v] of Object.entries({
249
+ ...this.#headers,
250
+ ...call.headers
251
+ })) headers.set(k, v);
252
+ headers.set("authorization", `Bearer ${this.#apiKey}`);
253
+ const init = {
254
+ method: "POST",
255
+ headers,
256
+ body: JSON.stringify(body)
257
+ };
258
+ const started = Date.now();
259
+ for (let attempt = 1;; attempt++) {
260
+ if (call.signal?.aborted) throw call.signal.reason;
261
+ const outcome = await this.attempt(url, init, timeout, call.signal, attempt, started);
262
+ if (!(outcome instanceof MillisecondsError)) return outcome;
263
+ if (!outcome.retryable || attempt > maxRetries) throw outcome;
264
+ const wait = outcome.retryAfter === null ? backoff(attempt - 1) : Math.min(outcome.retryAfter, 60) * 1e3;
265
+ await sleep(wait, call.signal);
266
+ }
267
+ }
268
+ async attempt(url, init, timeout, signal, attempt, started) {
269
+ const guard = attemptSignal(timeout, signal);
270
+ try {
271
+ const response = await this.#fetch(url, {
272
+ ...init,
273
+ signal: guard.signal
274
+ });
275
+ if (response.ok) return {
276
+ response,
277
+ raw: await response.json()
278
+ };
279
+ return await httpError(response, attempt, Date.now() - started);
280
+ } catch (cause) {
281
+ if (signal?.aborted) throw cause;
282
+ return transportError(cause, attempt, Date.now() - started);
283
+ } finally {
284
+ guard.done();
285
+ }
286
+ }
287
+ };
288
+ function transportError(cause, attempts, ms) {
289
+ const name = cause?.name ?? "";
290
+ const code = name === "TimeoutError" || name === "AbortError" ? "timeout" : "connection_error";
291
+ const apiMessage = cause?.message ?? String(cause);
292
+ return new MillisecondsError({
293
+ code,
294
+ status: 0,
295
+ apiMessage,
296
+ message: describe(code, 0, apiMessage, attempts, ms),
297
+ attempts,
298
+ retryable: true,
299
+ cause
300
+ });
301
+ }
302
+ async function httpError(response, attempts, ms) {
303
+ const body = await response.clone().json().catch(() => null);
304
+ const code = body?.error?.code ?? (response.status >= 500 ? "internal_error" : "http_error");
305
+ const apiMessage = body?.error?.message ?? response.statusText;
306
+ const retryAfter = response.headers.get("retry-after");
307
+ return new MillisecondsError({
308
+ code,
309
+ status: response.status,
310
+ apiMessage,
311
+ message: describe(code, response.status, apiMessage, attempts, ms),
312
+ retryAfter: retryAfter === null ? null : Number(retryAfter) || 0,
313
+ rateLimit: rateLimitOf(response.headers),
314
+ response,
315
+ attempts,
316
+ retryable: RETRY.has(code) || response.status >= 502 && response.status <= 504
317
+ });
318
+ }
319
+ //#endregion
320
+ //#region src/schema.ts
321
+ const CONVERT = "Pass a JSON Schema. zod: typed<z.infer<typeof S>>(z.toJSONSchema(S)). valibot: typed<v.InferOutput<typeof S>>(toJsonSchema(S)). arktype works directly.";
322
+ /**
323
+ * The object to send. A Standard Schema with a converter method (arktype) is converted.
324
+ * A Standard Schema without one cannot be: the SDK has zero dependencies, so it names the
325
+ * one line that converts it instead of importing zod.
326
+ */
327
+ function toJsonSchema(schema) {
328
+ const s = schema;
329
+ const convert = typeof s.toJsonSchema === "function" ? s.toJsonSchema : s.toJSONSchema;
330
+ if (typeof convert === "function") return convert.call(s);
331
+ if ("~standard" in s) throw new MillisecondsError({
332
+ code: "invalid_schema",
333
+ status: 0,
334
+ apiMessage: CONVERT
335
+ });
336
+ return s;
337
+ }
338
+ //#endregion
339
+ //#region src/decision-machine.ts
340
+ const MODEL = "decision-machine-1";
341
+ /** The batch shape follows the request, never the response. */
342
+ const same = (raw) => raw;
343
+ const key = (name) => (raw) => raw[name];
344
+ /** `text` for one result, `texts` for a batch. The mutual exclusion is impossible here. */
345
+ const inputBody = (input) => typeof input === "string" ? { text: input } : { texts: input };
346
+ /**
347
+ * `decision-machine-1` at api.milliseconds.ai. Every capability is a pure function, so
348
+ * every retry is safe.
349
+ */
350
+ var DecisionMachine = class extends Client {
351
+ /** Paths are `${baseUrl}/v1/${model}/<capability>`. */
352
+ static model = MODEL;
353
+ /**
354
+ * Answers each statement with yes or no and a probability. The statements share one
355
+ * inference call, so extra statements are nearly free.
356
+ */
357
+ yesNo(input, statements, options) {
358
+ checkInput(input);
359
+ checkSpec("statements", statements);
360
+ const { when_true, when_false, ...call } = options ?? {};
361
+ const body = {
362
+ ...inputBody(input),
363
+ ...typeof statements === "string" ? { statement: statements } : { statements },
364
+ ...when_true === void 0 ? {} : { when_true },
365
+ ...when_false === void 0 ? {} : { when_false }
366
+ };
367
+ return this.capability("yes-no", body, input, call, typeof statements === "string" ? same : key("results"));
368
+ }
369
+ /** Picks one label and returns the full distribution. Describe each label. */
370
+ classify(input, labels, options) {
371
+ checkInput(input);
372
+ checkList("labels", labels, 2, 64, "classify");
373
+ return this.capability("classify", {
374
+ ...inputBody(input),
375
+ labels
376
+ }, input, options, same);
377
+ }
378
+ /**
379
+ * Runs classify once per level of a nested tree, descending into the winner.
380
+ *
381
+ * The per-level `input_chars` and `input_tokens` do not sum to `Usage.inputChars`: the
382
+ * header counts one pass over the body, and each level re-sends the text.
383
+ */
384
+ classifyTree(input, tree, options) {
385
+ checkInput(input);
386
+ checkList("labels", tree, 2, 64, "classify-tree", true);
387
+ return this.capability("classify-tree", {
388
+ ...inputBody(input),
389
+ tree
390
+ }, input, options, same);
391
+ }
392
+ /** Places the text on an ordered scale of described levels, low to high. */
393
+ rate(input, scale, options) {
394
+ checkInput(input);
395
+ checkList("scale", scale, 2, 10, "rate");
396
+ return this.capability("rate", {
397
+ ...inputBody(input),
398
+ scale
399
+ }, input, options, same);
400
+ }
401
+ /** Quotes the answer out of the text, with its offsets. `answer` is null when nothing fits. */
402
+ answer(input, questions, options) {
403
+ checkInput(input);
404
+ checkSpec("questions", questions);
405
+ const body = {
406
+ ...inputBody(input),
407
+ ...typeof questions === "string" ? { question: questions } : { questions }
408
+ };
409
+ return this.capability("answer", body, input, options, typeof questions === "string" ? same : key("results"));
410
+ }
411
+ /**
412
+ * Fills a JSON Schema from the text. Missing values are null, arrays of objects come back
413
+ * empty, arrays of scalars come back as strings, and enums are not checked server side.
414
+ */
415
+ extract(input, schema, options) {
416
+ checkInput(input);
417
+ const json = toJsonSchema(schema);
418
+ checkSchema(json);
419
+ return this.capability("extract", {
420
+ ...inputBody(input),
421
+ schema: json
422
+ }, input, options, key("data"));
423
+ }
424
+ /** Finds every span matching each type, with offsets, sorted by start. */
425
+ entities(input, types, options) {
426
+ checkInput(input);
427
+ checkList("types", types, 1, 64, "entities");
428
+ return this.capability("entities", {
429
+ ...inputBody(input),
430
+ types
431
+ }, input, options, key("entities"));
432
+ }
433
+ /** Checks whether the text says `value` for `field`. */
434
+ verify(input, field, value, options) {
435
+ checkInput(input);
436
+ const body = {
437
+ ...inputBody(input),
438
+ field: typeof field === "string" ? { name: field } : field,
439
+ value
440
+ };
441
+ return this.capability("verify", body, input, options, same);
442
+ }
443
+ /** `{ results }` is unwrapped for a batch, and the per-text envelope for one text. */
444
+ capability(name, body, input, options, one) {
445
+ const unwrap = typeof input === "string" ? one : (raw) => (raw.results ?? []).map(one);
446
+ return this.call(`/v1/${MODEL}/${name}`, body, options, unwrap);
447
+ }
448
+ };
449
+ //#endregion
450
+ //#region src/cli/print.ts
451
+ const num = (r, k) => Number(r[k] ?? 0);
452
+ const text = (r, k) => String(r[k] ?? "");
453
+ const p3 = (n) => n.toFixed(3);
454
+ const int = (n) => n.toLocaleString("en-US");
455
+ /** ANSI off for --no-color, for NO_COLOR, and for a pipe. */
456
+ const dimmer = (color) => (s) => color ? `\u001B[2m${s}\u001B[0m` : s;
457
+ /** `label billing` — a padded key column. */
458
+ const kv = (rows, dim, width = 12) => rows.map(([k, v]) => `${dim(k.padEnd(width))} ${v}`).join("\n");
459
+ /** 20 blocks at 1.0. 0.001 rounds to none, so a flat score prints no bar. */
460
+ const bar = (p) => "█".repeat(Math.round(p * 20));
461
+ /** The scores block under a table. `sort` orders by score; a scale keeps its own order. */
462
+ function scores(entries, dim, sort) {
463
+ const rows = sort ? [...entries].sort((a, b) => b[1] - a[1]) : entries;
464
+ const width = Math.max(0, ...rows.map(([name]) => name.length)) + 3;
465
+ const body = rows.map(([name, p]) => {
466
+ const drawn = bar(p);
467
+ return ` ${name.padEnd(width)}${p3(p)}${drawn ? ` ${drawn}` : ""}`;
468
+ });
469
+ return [dim("scores"), ...body].join("\n");
470
+ }
471
+ /** An aligned block of columns, two spaces apart. */
472
+ function columns(rows) {
473
+ const width = [];
474
+ for (const row of rows) row.forEach((cell, i) => width[i] = Math.max(width[i] ?? 0, cell.length));
475
+ return rows.map((row) => row.map((cell, i) => i === row.length - 1 ? cell : cell.padEnd(width[i] ?? 0)).join(" ").trimEnd()).join("\n");
476
+ }
477
+ //#endregion
478
+ //#region src/cli/spec.ts
479
+ /** Bad arguments or bad stdin. Nothing was sent, so `dm1` exits 2. */
480
+ var UsageError = class extends Error {};
481
+ const specSize = (list) => Array.isArray(list) ? list.length : Object.keys(list).length;
482
+ /** Reads a file, or stdin for `-`. */
483
+ function read(path, stdin) {
484
+ if (path === "-") return stdin();
485
+ try {
486
+ return readFileSync(path, "utf8");
487
+ } catch (e) {
488
+ throw new UsageError(`cannot read ${path}: ${e.message}`);
489
+ }
490
+ }
491
+ /** `@labels.json` and `--schema @invoice.json`. `@-` reads stdin. */
492
+ function loadJson(path, stdin) {
493
+ const raw = read(path, stdin);
494
+ try {
495
+ return JSON.parse(raw);
496
+ } catch (e) {
497
+ throw new UsageError(`${path === "-" ? "stdin" : path} is not JSON: ${e.message}`);
498
+ }
499
+ }
500
+ /** `--schema` and `--tree` take a file, `@file`, `@-` or inline JSON. */
501
+ function loadDocument(flag, value, stdin) {
502
+ if (value.trimStart().startsWith("{")) try {
503
+ return JSON.parse(value);
504
+ } catch (e) {
505
+ throw new UsageError(`--${flag} is not JSON: ${e.message}`);
506
+ }
507
+ return loadJson(value.replace(/^@/, ""), stdin);
508
+ }
509
+ /**
510
+ * Turns the positionals into the capability's list.
511
+ *
512
+ * `described` splits `name=description` on the first `=`. yes-no, answer and rate take free
513
+ * text, so they never split. `@path` loads a JSON array or a name-to-description object.
514
+ */
515
+ function parseSpec(items, described, stdin) {
516
+ const names = [];
517
+ const map = {};
518
+ let mapped = false;
519
+ for (const item of items) {
520
+ if (item.startsWith("@")) {
521
+ const loaded = loadJson(item.slice(1), stdin);
522
+ if (Array.isArray(loaded)) names.push(...loaded.map(String));
523
+ else if (typeof loaded === "object" && loaded !== null) {
524
+ if (!described) throw new UsageError(`${item.slice(1)} holds a name-to-description object. yes-no, rate and answer take a JSON array.`);
525
+ mapped = true;
526
+ for (const [k, v] of Object.entries(loaded)) map[k] = String(v);
527
+ } else throw new UsageError(`${item.slice(1)} must hold an array or a name-to-description object.`);
528
+ continue;
529
+ }
530
+ const at = described ? item.indexOf("=") : -1;
531
+ if (at > 0) {
532
+ mapped = true;
533
+ map[item.slice(0, at)] = item.slice(at + 1);
534
+ } else names.push(item);
535
+ }
536
+ if (!mapped) return {
537
+ list: names,
538
+ bare: true
539
+ };
540
+ for (const name of names) map[name] ??= "";
541
+ return {
542
+ list: map,
543
+ bare: false
544
+ };
545
+ }
546
+ //#endregion
547
+ //#region src/cli/commands.ts
548
+ /** The flags, for node:util parseArgs. */
549
+ const OPTIONS = {
550
+ json: { type: "boolean" },
551
+ jsonl: { type: "boolean" },
552
+ raw: { type: "boolean" },
553
+ quiet: {
554
+ type: "boolean",
555
+ short: "q"
556
+ },
557
+ check: { type: "boolean" },
558
+ min: { type: "string" },
559
+ "min-confidence": { type: "string" },
560
+ key: { type: "string" },
561
+ "base-url": { type: "string" },
562
+ retries: { type: "string" },
563
+ timeout: { type: "string" },
564
+ usage: {
565
+ type: "boolean",
566
+ short: "v"
567
+ },
568
+ "no-color": { type: "boolean" },
569
+ help: {
570
+ type: "boolean",
571
+ short: "h"
572
+ },
573
+ version: {
574
+ type: "boolean",
575
+ short: "V"
576
+ },
577
+ file: {
578
+ type: "string",
579
+ short: "f",
580
+ multiple: true
581
+ },
582
+ lines: { type: "string" },
583
+ schema: { type: "string" },
584
+ tree: { type: "string" },
585
+ field: { type: "string" },
586
+ value: { type: "string" },
587
+ "when-true": { type: "string" },
588
+ "when-false": { type: "string" }
589
+ };
590
+ const need = (v, flag, example) => {
591
+ if (v === void 0 || v === "") throw new UsageError(`${flag} is required. For example: ${example}`);
592
+ return v;
593
+ };
594
+ const list = (l) => Array.isArray(l) ? l : Object.keys(l);
595
+ const asRow = (r) => r;
596
+ const asRows = (r) => r;
597
+ /** `field` takes a bare name or `name=description`. The SDK sends `{ name }` for a bare one. */
598
+ function fieldOf(raw) {
599
+ const at = raw.indexOf("=");
600
+ return at > 0 ? {
601
+ name: raw.slice(0, at),
602
+ description: raw.slice(at + 1)
603
+ } : raw;
604
+ }
605
+ const span = (r) => r.start === null ? "" : `${num(r, "start")}-${num(r, "end")}`;
606
+ const HELP = `dm1 — typed decisions over text, from milliseconds.ai
607
+
608
+ USAGE
609
+ dm1 <capability> [text] [args...] [options]
610
+ <command> | dm1 <capability> [args...]
611
+
612
+ CAPABILITIES
613
+ classify <text> <label[=description]>... Pick one label
614
+ yes-no <text> <statement>... True or false, per statement
615
+ rate <text> <level>... Place the text on a low-to-high scale
616
+ answer <text> <question>... Quote the answer out of the text
617
+ entities <text> <type[=description]>... Find every mention, with offsets
618
+ extract <text> --schema <file|json> Fill a JSON Schema
619
+ verify <text> --field <name> --value <v> Check a value against the text
620
+ classify-tree <text> --tree <file|json> Walk a nested label tree
621
+ check Test the key. Print the limits.
622
+
623
+ TEXT
624
+ [text] The text itself. Omit it, or pass -, to read stdin.
625
+ -f, --file <path> Read the text from a file. Repeat it for a batch, in order.
626
+ --lines <path> One text per line. Sent in chunks of 32, in order.
627
+ Piped input that starts with { becomes the whole request body. Flags still win.
628
+
629
+ SPECIFICATION
630
+ A positional after the text is a label, statement, level, question or type.
631
+ name=description splits on the first =. A bare name has no description.
632
+ Any list also loads from a file: @labels.json holds an array or a name-to-description object.
633
+ --tree @taxonomy.json --schema @invoice.json @- reads stdin.
634
+ --when-true <s> --when-false <s> yes-no hints
635
+ --field <name[=description]> --value <v> verify
636
+
637
+ OPTIONS
638
+ --json Print JSON, even on a terminal.
639
+ --jsonl One compact JSON result per line. For pipes.
640
+ --raw Print the result with its API envelope.
641
+ -q, --quiet Print the primary value only.
642
+ --check yes-no and verify only. Exit 3 when the answer is no.
643
+ --min <p> Exit 3 when probability is below <p>. Not rate or extract.
644
+ --min-confidence <c> Exit 3 when confidence is below <c>. classify, rate and
645
+ classify-tree only.
646
+ --key <key> API key. Default: $MS_API_KEY.
647
+ --base-url <url> Default: https://api.milliseconds.ai
648
+ --retries <n> Retries on 429, 502, 529 and network errors. Default 2.
649
+ --timeout <ms> Per attempt. Default 60000.
650
+ -v, --usage Print tokens, model time and rate limits to stderr.
651
+ --no-color No ANSI. NO_COLOR and a non-TTY stdout do the same.
652
+ -h, --help This text. After a capability, that capability's help.
653
+ -V, --version
654
+
655
+ DESCRIBE YOUR LABELS
656
+ The label text is the instruction. The model reads it literally.
657
+ dm1 classify "$T" billing shipping works
658
+ dm1 classify "$T" billing="charges and refunds" shipping="delivery and tracking"
659
+ The second call scores measurably better. Use name=description everywhere.
660
+
661
+ EXIT CODES
662
+ 0 decided 2 bad usage
663
+ 1 API error 3 --check or --min failed
664
+
665
+ EXAMPLES
666
+ dm1 classify "I was charged twice" billing shipping account
667
+ dm1 yes-no "Ship it today" "The customer expresses urgency." --check
668
+ dm1 rate "This is unacceptable" Calm Annoyed Angry "Threatening to leave"
669
+ dm1 entities "Ada met Grace in Paris" person place --json | jq -r '.[].text'
670
+ dm1 extract -f invoice.txt --schema @invoice.json
671
+ dm1 classify --lines tickets.txt billing shipping account --jsonl > labelled.ndjson
672
+ pbpaste | dm1 classify @labels.json
673
+
674
+ No key yet? https://console.milliseconds.ai then export MS_API_KEY=sk-ms-...
675
+ `;
676
+ const CHECK_HELP = `
677
+ dm1 check [options]
678
+
679
+ Tests the key and prints the limits. It sends one small classify, about 20 tokens.
680
+ Do not run it in a health-check loop.
681
+
682
+ EXAMPLES
683
+ dm1 check
684
+ dm1 check --base-url https://api.milliseconds.ai --key sk-ms-...
685
+ `;
686
+ const CAPABILITIES = {
687
+ classify: {
688
+ name: "classify",
689
+ described: true,
690
+ envelope: null,
691
+ checkable: false,
692
+ probability: true,
693
+ confidence: true,
694
+ hint: "dm1 classify \"I was charged twice.\" billing=\"charges and refunds\" shipping=\"delivery\"",
695
+ help: `
696
+ dm1 classify [text] <label[=description]>... [options]
697
+
698
+ Picks one label and returns the full distribution.
699
+ POST /v1/decision-machine-1/classify
700
+
701
+ ARGUMENTS
702
+ [text] The text. Omit it, or pass -, to read stdin.
703
+ <label[=description]> 2 to 64 labels. Describe each one for better accuracy.
704
+ @labels.json loads an array or a name-to-description object.
705
+
706
+ RESULT
707
+ label, probability, confidence, scores
708
+
709
+ PREDICATES
710
+ --min <p> Exit 3 when probability is below p.
711
+ --min-confidence <c> Exit 3 when confidence is below c.
712
+
713
+ EXAMPLES
714
+ dm1 classify "I was charged twice." billing="charges and refunds" shipping="delivery"
715
+ dm1 classify -f ticket.txt @labels.json --json
716
+ dm1 classify --lines tickets.txt @labels.json --jsonl | jq -r '.label'
717
+ cat ticket.txt | dm1 classify @labels.json --min 0.9 -q
718
+ `,
719
+ call: (c) => c.dm.classify(c.input, c.list),
720
+ fragment: (c) => specSize(c.list) > 0 ? { labels: c.list } : {},
721
+ table: (r, dim) => {
722
+ const o = asRow(r);
723
+ return `${kv([
724
+ ["label", text(o, "label")],
725
+ ["probability", p3(num(o, "probability"))],
726
+ ["confidence", p3(num(o, "confidence"))]
727
+ ], dim)}\n\n${scores(Object.entries(o.scores), dim, true)}`;
728
+ },
729
+ quiet: (r) => [text(asRow(r), "label")]
730
+ },
731
+ "yes-no": {
732
+ name: "yes-no",
733
+ described: false,
734
+ envelope: "results",
735
+ checkable: true,
736
+ probability: true,
737
+ confidence: false,
738
+ hint: "dm1 yes-no \"Fix this today.\" \"The customer expresses urgency.\"",
739
+ help: `
740
+ dm1 yes-no [text] <statement>... [options]
741
+
742
+ Answers each statement with yes or no and a probability.
743
+ POST /v1/decision-machine-1/yes-no
744
+
745
+ ARGUMENTS
746
+ [text] The text. Omit it, or pass -, to read stdin.
747
+ <statement> 1 to 32 third-person claims about the text. They share one
748
+ inference call, so extra statements are nearly free.
749
+
750
+ FLAGS
751
+ --when-true <s> What makes the statement true. It improves accuracy.
752
+ --when-false <s> What makes it false.
753
+ --check Exit 3 when the answer is no. In a batch, any no fails the run.
754
+
755
+ RESULT
756
+ statement, answer, probability
757
+
758
+ EXAMPLES
759
+ dm1 yes-no "Fix this today." "The customer expresses urgency."
760
+ dm1 yes-no -f reply.txt "The reply promises a refund." --check --min 0.9 -q
761
+ dm1 yes-no -f a.txt -f b.txt "The text mentions a price." --jsonl
762
+ `,
763
+ call: (c) => c.dm.yesNo(c.input, list(c.list), {
764
+ ...c.v["when-true"] === void 0 ? {} : { when_true: c.v["when-true"] },
765
+ ...c.v["when-false"] === void 0 ? {} : { when_false: c.v["when-false"] }
766
+ }),
767
+ fragment: (c) => ({
768
+ ...specSize(c.list) > 0 ? {
769
+ statements: list(c.list),
770
+ statement: void 0
771
+ } : {},
772
+ ...c.v["when-true"] === void 0 ? {} : { when_true: c.v["when-true"] },
773
+ ...c.v["when-false"] === void 0 ? {} : { when_false: c.v["when-false"] }
774
+ }),
775
+ table: (r) => columns(asRows(r).map((o) => [
776
+ o.answer === true ? "yes" : "no",
777
+ p3(num(o, "probability")),
778
+ text(o, "statement")
779
+ ])),
780
+ quiet: (r) => asRows(r).map((o) => o.answer === true ? "yes" : "no")
781
+ },
782
+ rate: {
783
+ name: "rate",
784
+ described: false,
785
+ envelope: null,
786
+ checkable: false,
787
+ probability: false,
788
+ confidence: true,
789
+ hint: "dm1 rate \"I am done with this company.\" Calm Annoyed Angry \"Threatening to leave\"",
790
+ help: `
791
+ dm1 rate [text] <level>... [options]
792
+
793
+ Places the text on an ordered scale of described levels.
794
+ POST /v1/decision-machine-1/rate
795
+
796
+ ARGUMENTS
797
+ [text] The text. Omit it, or pass -, to read stdin.
798
+ <level> 2 to 10 level descriptions, low to high, in order. Never numbers.
799
+
800
+ RESULT
801
+ score, level, label, confidence, scores
802
+ Route on score and confidence. level flips on 0.001.
803
+
804
+ EXAMPLES
805
+ dm1 rate "I am done with this company." Calm Annoyed Angry "Threatening to leave"
806
+ dm1 rate -f email.txt @scale.json --usage
807
+ `,
808
+ call: (c) => c.dm.rate(c.input, list(c.list)),
809
+ fragment: (c) => specSize(c.list) > 0 ? { scale: list(c.list) } : {},
810
+ table: (r, dim, names) => {
811
+ const o = asRow(r);
812
+ const scale = o.scores.map((p, i) => [names[i] ?? `${i}`, p]);
813
+ return `${kv([
814
+ ["label", text(o, "label")],
815
+ ["score", num(o, "score").toFixed(2)],
816
+ ["level", text(o, "level")],
817
+ ["confidence", p3(num(o, "confidence"))]
818
+ ], dim)}\n\n${scores(scale, dim, false)}`;
819
+ },
820
+ quiet: (r) => [text(asRow(r), "label")]
821
+ },
822
+ answer: {
823
+ name: "answer",
824
+ described: false,
825
+ envelope: "results",
826
+ checkable: false,
827
+ probability: true,
828
+ confidence: false,
829
+ hint: "dm1 answer -f press.txt \"Who announced the product?\" \"How much does it cost?\"",
830
+ help: `
831
+ dm1 answer [text] <question>... [options]
832
+
833
+ Quotes the answer out of the text, with its offsets.
834
+ POST /v1/decision-machine-1/answer
835
+
836
+ ARGUMENTS
837
+ [text] The text. Omit it, or pass -, to read stdin.
838
+ <question> 1 to 32 questions. They share one inference call.
839
+ Name the role, not the type: "the date the payment is due".
840
+
841
+ RESULT
842
+ question, answer, probability, start, end
843
+ answer is null when nothing fits. start and end are then null too.
844
+ Offsets index the text at the same position in the batch, never a joined string.
845
+
846
+ EXAMPLES
847
+ dm1 answer -f press.txt "Who announced the product?" "How much does it cost?"
848
+ curl -s https://example.com/press.txt | dm1 answer - "Who announced the product?"
849
+ `,
850
+ call: (c) => c.dm.answer(c.input, list(c.list)),
851
+ fragment: (c) => specSize(c.list) > 0 ? {
852
+ questions: list(c.list),
853
+ question: void 0
854
+ } : {},
855
+ table: (r) => columns(asRows(r).map((o) => [
856
+ o.answer === null ? "-" : text(o, "answer"),
857
+ p3(num(o, "probability")),
858
+ span(o),
859
+ text(o, "question")
860
+ ])),
861
+ quiet: (r) => asRows(r).map((o) => o.answer === null ? "" : text(o, "answer"))
862
+ },
863
+ entities: {
864
+ name: "entities",
865
+ described: true,
866
+ envelope: "entities",
867
+ checkable: false,
868
+ probability: true,
869
+ confidence: false,
870
+ hint: "dm1 entities \"Ada met Grace in Paris.\" person=\"a human name\" place=\"a city or country\"",
871
+ help: `
872
+ dm1 entities [text] <type[=description]>... [options]
873
+
874
+ Finds every span matching each type, with offsets.
875
+ POST /v1/decision-machine-1/entities
876
+
877
+ ARGUMENTS
878
+ [text] The text. Omit it, or pass -, to read stdin.
879
+ <type[=description]> 1 to 64 entity types. Describe each one.
880
+
881
+ RESULT
882
+ A list of { type, text, probability, start, end }, sorted by start.
883
+
884
+ EXAMPLES
885
+ dm1 entities "Ada met Grace in Paris." person="a human name" place="a city or country"
886
+ pbpaste | dm1 entities @types.json --json | jq -r '.[] | select(.type=="person") | .text'
887
+ `,
888
+ call: (c) => c.dm.entities(c.input, c.list),
889
+ fragment: (c) => specSize(c.list) > 0 ? { types: c.list } : {},
890
+ table: (r) => columns(asRows(r).map((o) => [
891
+ text(o, "type"),
892
+ text(o, "text"),
893
+ p3(num(o, "probability")),
894
+ span(o)
895
+ ])),
896
+ quiet: (r) => asRows(r).map((o) => text(o, "text"))
897
+ },
898
+ extract: {
899
+ name: "extract",
900
+ described: true,
901
+ envelope: "data",
902
+ checkable: false,
903
+ probability: false,
904
+ confidence: false,
905
+ hint: "dm1 extract -f invoice.txt --schema @invoice.json --json > invoice.json",
906
+ help: `
907
+ dm1 extract [text] --schema <file|json> [options]
908
+
909
+ Fills a JSON Schema from the text.
910
+ POST /v1/decision-machine-1/extract
911
+
912
+ FLAGS
913
+ --schema @invoice.json The JSON Schema. @- reads stdin. Inline JSON works too.
914
+
915
+ RESULT
916
+ The data object. Missing values are null. Arrays of objects come back empty.
917
+ Arrays of scalars come back as strings. Enums are not checked server side.
918
+
919
+ EXAMPLES
920
+ dm1 extract -f invoice.txt --schema @invoice.json --json > invoice.json
921
+ dm1 extract -f a.txt -f b.txt --schema @invoice.json --jsonl
922
+ `,
923
+ call: (c) => c.dm.extract(c.input, loadDocument("schema", need(c.v.schema, "--schema", "dm1 extract -f invoice.txt --schema @invoice.json"), c.stdin)),
924
+ fragment: (c) => c.v.schema === void 0 ? {} : { schema: loadDocument("schema", c.v.schema, c.stdin) },
925
+ table: (r) => columns(Object.entries(asRow(r)).map(([k, v]) => [k, v === null ? "-" : typeof v === "object" ? JSON.stringify(v) : String(v)])),
926
+ quiet: (r) => [JSON.stringify(r)]
927
+ },
928
+ verify: {
929
+ name: "verify",
930
+ described: false,
931
+ envelope: null,
932
+ checkable: true,
933
+ probability: true,
934
+ confidence: false,
935
+ hint: "dm1 verify -f invoice.txt --field total --value 999 --json | jq .found",
936
+ help: `
937
+ dm1 verify [text] --field <name[=description]> --value <v> [options]
938
+
939
+ Checks whether the text says <v> for <field>.
940
+ POST /v1/decision-machine-1/verify
941
+
942
+ FLAGS
943
+ --field <name[=description]> The field to read out of the text.
944
+ --value <v> The value you already hold.
945
+ --check Exit 3 when the value does not match.
946
+
947
+ RESULT
948
+ matches, probability, found
949
+ found shows what the text actually says.
950
+
951
+ EXAMPLES
952
+ dm1 verify -f invoice.txt --field invoice_number="the identifier printed on the invoice" \\
953
+ --value 4471 --check
954
+ dm1 verify -f invoice.txt --field total --value 999 --json | jq .found
955
+ `,
956
+ call: (c) => c.dm.verify(c.input, fieldOf(need(c.v.field, "--field", "dm1 verify -f invoice.txt --field total --value 999")), need(c.v.value, "--value", "dm1 verify -f invoice.txt --field total --value 999")),
957
+ fragment: (c) => ({
958
+ ...c.v.field === void 0 ? {} : { field: fieldOf(c.v.field) },
959
+ ...c.v.value === void 0 ? {} : { value: c.v.value }
960
+ }),
961
+ table: (r, dim) => {
962
+ const o = asRow(r);
963
+ return kv([
964
+ ["matches", o.matches === true ? "yes" : "no"],
965
+ ["probability", p3(num(o, "probability"))],
966
+ ["found", o.found.join(", ")]
967
+ ], dim);
968
+ },
969
+ quiet: (r) => [asRow(r).matches === true ? "yes" : "no"]
970
+ },
971
+ "classify-tree": {
972
+ name: "classify-tree",
973
+ described: false,
974
+ envelope: null,
975
+ checkable: false,
976
+ probability: true,
977
+ confidence: true,
978
+ hint: "dm1 classify-tree -f ticket.txt --tree @taxonomy.json --json",
979
+ help: `
980
+ dm1 classify-tree [text] --tree <file|json> [options]
981
+
982
+ Runs classify once per level of a nested tree, descending into the winner.
983
+ POST /v1/decision-machine-1/classify-tree
984
+
985
+ FLAGS
986
+ --tree @taxonomy.json name -> description, or name -> { description, labels }.
987
+ 2 to 64 labels per level, at most 8 levels.
988
+
989
+ RESULT
990
+ path, label, probability, confidence, levels[]
991
+ probability and confidence are products over the levels, so they fall with depth.
992
+ Each level re-sends the text, so the per-level input numbers do not sum to the
993
+ x-input-chars header.
994
+
995
+ EXAMPLES
996
+ dm1 classify-tree -f ticket.txt --tree @taxonomy.json --json
997
+ dm1 classify-tree "I want my money back." --tree @taxonomy.json -q
998
+ `,
999
+ call: (c) => c.dm.classifyTree(c.input, loadDocument("tree", need(c.v.tree, "--tree", "dm1 classify-tree -f ticket.txt --tree @taxonomy.json"), c.stdin)),
1000
+ fragment: (c) => c.v.tree === void 0 ? {} : { tree: loadDocument("tree", c.v.tree, c.stdin) },
1001
+ table: (r, dim) => {
1002
+ const o = asRow(r);
1003
+ const levels = o.levels;
1004
+ const head = kv([
1005
+ ["label", text(o, "label")],
1006
+ ["path", o.path.join(" > ")],
1007
+ ["probability", p3(num(o, "probability"))],
1008
+ ["confidence", p3(num(o, "confidence"))]
1009
+ ], dim);
1010
+ const body = columns(levels.map((l, i) => [
1011
+ ` ${i + 1}`,
1012
+ text(l, "label"),
1013
+ p3(num(l, "probability")),
1014
+ p3(num(l, "confidence")),
1015
+ `${int(num(l, "inference_ms"))} ms`
1016
+ ]));
1017
+ return `${head}\n\n${dim("levels")}\n${body}`;
1018
+ },
1019
+ quiet: (r) => [text(asRow(r), "label")]
1020
+ }
1021
+ };
1022
+ //#endregion
1023
+ //#region src/cli/input.ts
1024
+ /** One trailing newline is a file ending, not text. It would bill and print. */
1025
+ function readFile(path) {
1026
+ try {
1027
+ return readFileSync(path, "utf8").replace(/\r?\n$/, "");
1028
+ } catch (e) {
1029
+ throw new UsageError(`cannot read ${path}: ${e.message}`);
1030
+ }
1031
+ }
1032
+ const NO_TEXT = "no text. Pass the text, or -f <file>, or --lines <file>, or pipe the text in.";
1033
+ /** A @file and a name=description are list arguments. Anything else could be the text. */
1034
+ const listOnly = (item) => item.startsWith("@") || item.includes("=");
1035
+ /** The pipe wins over a positional, so say which argument lost the text. */
1036
+ const shadowed = (first) => `dm1: the text comes from stdin. "${first.length > 32 ? `${first.slice(0, 31)}…` : first}" is a list argument, not the text. Pass - to silence this.`;
1037
+ /**
1038
+ * One source only.
1039
+ *
1040
+ * `--file` and `--lines` win. A leading `-` reads stdin. A pipe that carries text reads
1041
+ * stdin, and every positional then belongs to the capability's list. Otherwise the first
1042
+ * positional is the text. No text anywhere is a usage error.
1043
+ */
1044
+ function resolveInput(s) {
1045
+ if (s.lines !== void 0) {
1046
+ const texts = readFile(s.lines).split(/\r?\n/).filter((line) => line.trim() !== "");
1047
+ if (texts.length === 0) throw new UsageError(`${s.lines} holds no text.`);
1048
+ return {
1049
+ input: texts,
1050
+ items: s.positionals
1051
+ };
1052
+ }
1053
+ if (s.files.length > 0) {
1054
+ const texts = s.files.map(readFile);
1055
+ return {
1056
+ input: texts.length === 1 ? texts[0] : texts,
1057
+ items: s.positionals
1058
+ };
1059
+ }
1060
+ let items = s.positionals;
1061
+ let explicit = false;
1062
+ if (items[0] === "-") {
1063
+ explicit = true;
1064
+ items = items.slice(1);
1065
+ }
1066
+ const raw = explicit || !s.stdinIsTty ? s.stdin() : "";
1067
+ const trimmed = raw.trim();
1068
+ if (trimmed === "") {
1069
+ if (explicit) throw new UsageError("stdin is empty. Send the text in, or pass -f <file>.");
1070
+ if (items.length === 0) throw new UsageError(NO_TEXT);
1071
+ return {
1072
+ input: items[0],
1073
+ items: items.slice(1)
1074
+ };
1075
+ }
1076
+ if (!trimmed.startsWith("{")) {
1077
+ const first = items[0];
1078
+ const quiet = explicit || first === void 0 || listOnly(first);
1079
+ return {
1080
+ input: raw.replace(/\r?\n$/, ""),
1081
+ items,
1082
+ ...quiet ? {} : { note: shadowed(first) }
1083
+ };
1084
+ }
1085
+ let body;
1086
+ try {
1087
+ body = JSON.parse(trimmed);
1088
+ } catch (e) {
1089
+ throw new UsageError(`stdin starts with { but is not JSON: ${e.message}. Fix the JSON, or send plain text with -f <file>.`);
1090
+ }
1091
+ if (Array.isArray(body.texts)) {
1092
+ if (body.texts.length === 0) throw new UsageError("the piped body has an empty texts array. Send at least one text.");
1093
+ return {
1094
+ input: body.texts,
1095
+ items,
1096
+ body
1097
+ };
1098
+ }
1099
+ if (typeof body.text !== "string") throw new UsageError("the piped body has no text and no texts. Add one of them to the JSON.");
1100
+ return {
1101
+ input: body.text,
1102
+ items,
1103
+ body
1104
+ };
1105
+ }
1106
+ function chunk(texts) {
1107
+ const out = [];
1108
+ for (let i = 0; i < texts.length; i += 32) out.push(texts.slice(i, i + 32));
1109
+ return out;
1110
+ }
1111
+ //#endregion
1112
+ //#region src/cli/run.ts
1113
+ const VERSION = "0.1.0";
1114
+ const path = (name) => `/v1/decision-machine-1/${name}`;
1115
+ const wrapOne = (v, env) => env === null ? v : env === "results" ? { results: v } : env === "entities" ? { entities: v } : { data: v };
1116
+ const unwrapOne = (raw, env) => env === null ? raw : raw[env];
1117
+ const number = (flag, raw) => {
1118
+ const n = Number(raw);
1119
+ if (!Number.isFinite(n)) throw new UsageError(`${flag} takes a number, not ${raw}.`);
1120
+ return n;
1121
+ };
1122
+ /** Every result of one text: the object itself, or each element of a list. */
1123
+ const items = (result) => Array.isArray(result) ? result : [result];
1124
+ /** `dm1: <code>: <message>` on stderr, then the capability's first example. */
1125
+ function fail(io, e, cap) {
1126
+ const hint = () => {
1127
+ if (cap) io.err(`\n ${cap.hint}`);
1128
+ };
1129
+ if (e instanceof UsageError) {
1130
+ io.err(`dm1: usage: ${e.message}`);
1131
+ hint();
1132
+ return 2;
1133
+ }
1134
+ if (isMillisecondsError(e)) {
1135
+ io.err(`dm1: ${e.code}: ${e.message}`);
1136
+ const local = e.status === 0 && (e.code === "client_error" || e.code === "invalid_schema");
1137
+ if (local) hint();
1138
+ return local ? 2 : 1;
1139
+ }
1140
+ io.err(`dm1: internal_error: ${e.message}`);
1141
+ return 1;
1142
+ }
1143
+ async function run(io) {
1144
+ const found = {};
1145
+ try {
1146
+ return await dispatch(io, found);
1147
+ } catch (e) {
1148
+ return fail(io, e, found.cap);
1149
+ }
1150
+ }
1151
+ async function dispatch(io, found) {
1152
+ let parsed;
1153
+ try {
1154
+ parsed = parseArgs({
1155
+ args: io.argv,
1156
+ options: OPTIONS,
1157
+ allowPositionals: true
1158
+ });
1159
+ } catch (e) {
1160
+ throw new UsageError(`${e.message}\n Run dm1 --help.`);
1161
+ }
1162
+ const v = parsed.values;
1163
+ const [name, ...rest] = parsed.positionals;
1164
+ if (v.version) {
1165
+ io.out(VERSION);
1166
+ return 0;
1167
+ }
1168
+ if (name === void 0) {
1169
+ io.out(HELP);
1170
+ return 0;
1171
+ }
1172
+ const color = io.stdoutIsTty && v["no-color"] !== true && !io.env.NO_COLOR;
1173
+ if (name === "check") {
1174
+ if (v.help === true) {
1175
+ io.out(CHECK_HELP);
1176
+ return 0;
1177
+ }
1178
+ return await check(io, client(io, v), v, color);
1179
+ }
1180
+ const cap = CAPABILITIES[name];
1181
+ if (!cap) throw new UsageError(`unknown capability ${name}. Run dm1 --help.`);
1182
+ found.cap = cap;
1183
+ if (v.help === true) {
1184
+ io.out(cap.help);
1185
+ return 0;
1186
+ }
1187
+ if (v.check === true && !cap.checkable) throw new UsageError(`--check works with yes-no and verify only, not ${name}.`);
1188
+ const min = v.min === void 0 ? null : number("--min", v.min);
1189
+ const minConfidence = v["min-confidence"] === void 0 ? null : number("--min-confidence", v["min-confidence"]);
1190
+ if (min !== null && !cap.probability) throw new UsageError(`--min reads a probability. A ${name} result carries none.`);
1191
+ if (minConfidence !== null && !cap.confidence) throw new UsageError(`--min-confidence reads a confidence. A ${name} result carries none.`);
1192
+ const piped = io.argv.includes("-") || io.argv.includes("@-") || !io.stdinIsTty && v.lines === void 0 && (v.file ?? []).length === 0 ? await io.stdin() : "";
1193
+ const stdin = () => piped;
1194
+ const source = resolveInput({
1195
+ files: v.file ?? [],
1196
+ lines: v.lines,
1197
+ positionals: rest,
1198
+ stdin,
1199
+ stdinIsTty: io.stdinIsTty
1200
+ });
1201
+ if (source.note !== void 0) io.err(source.note);
1202
+ const spec = parseSpec(source.items, cap.described, stdin);
1203
+ const names = Array.isArray(spec.list) ? spec.list : Object.keys(spec.list);
1204
+ const bare = spec.bare ? names[0] : void 0;
1205
+ const dm = client(io, v);
1206
+ const mode = v.raw === true ? "raw" : v.jsonl === true ? "jsonl" : v.quiet === true ? "quiet" : v.json === true || !io.stdoutIsTty ? "json" : "table";
1207
+ const body = source.body ? {
1208
+ ...source.body,
1209
+ ...cap.fragment({
1210
+ dm,
1211
+ input: source.input,
1212
+ list: spec.list,
1213
+ v,
1214
+ stdin
1215
+ })
1216
+ } : null;
1217
+ const batch = Array.isArray(source.input);
1218
+ const env = body ? envelopeOf(cap, body) : cap.envelope;
1219
+ const pieces = batch ? chunk(source.input) : [source.input];
1220
+ const decided = [];
1221
+ const total = {
1222
+ chars: 0,
1223
+ tokens: 0,
1224
+ ms: 0,
1225
+ rateLimit: null
1226
+ };
1227
+ const streaming = mode === "jsonl" || mode === "quiet";
1228
+ for (const [i, piece] of pieces.entries()) {
1229
+ let value;
1230
+ let usage;
1231
+ try {
1232
+ const got = await (body ? dm.post(path(cap.name), body) : cap.call({
1233
+ dm,
1234
+ input: piece,
1235
+ list: spec.list,
1236
+ v,
1237
+ stdin
1238
+ })).withUsage();
1239
+ usage = got.usage;
1240
+ value = body ? unwrapBody(got.result, env, batch) : got.result;
1241
+ } catch (e) {
1242
+ if (pieces.length > 1 && isMillisecondsError(e) && e.status !== 0) {
1243
+ if (!streaming) emit(io, decided, mode, cap, color, batch, bare, names);
1244
+ const first = i * 32 + 1;
1245
+ io.err(`dm1: ${e.code}: chunk ${i + 1} of ${pieces.length} (texts ${first}-${first + piece.length - 1}) failed.\n${e.message}`);
1246
+ if (v.usage === true) io.err(usageLine(total));
1247
+ return 1;
1248
+ }
1249
+ if (v.usage === true && total.chars > 0) io.err(usageLine(total));
1250
+ throw e;
1251
+ }
1252
+ total.chars += usage.inputChars;
1253
+ total.tokens += usage.inputTokens;
1254
+ total.ms += usage.inferenceMs;
1255
+ total.rateLimit = usage.rateLimit ?? total.rateLimit;
1256
+ const part = Array.isArray(piece) ? piece.map((t, k) => ({
1257
+ text: t,
1258
+ result: value[k],
1259
+ raw: wrapOne(value[k], env)
1260
+ })) : [{
1261
+ text: piece,
1262
+ result: value,
1263
+ raw: wrapOne(value, env)
1264
+ }];
1265
+ if (streaming) emit(io, part, mode, cap, color, batch, bare, names);
1266
+ decided.push(...part);
1267
+ }
1268
+ if (!streaming) emit(io, decided, mode, cap, color, batch, bare, names);
1269
+ if (v.usage === true) io.err(usageLine(total));
1270
+ return failed(decided, v, min, minConfidence, cap) ? 3 : 0;
1271
+ }
1272
+ /** The CLI never chunks a piped body: the user wrote it, so the SDK sends it unchanged. */
1273
+ function unwrapBody(raw, env, batch) {
1274
+ if (!batch) return unwrapOne(raw, env);
1275
+ return (raw.results ?? []).map((r) => unwrapOne(r, env));
1276
+ }
1277
+ /** A piped body picks its own shape: `statement` answers one result, `statements` a list. */
1278
+ function envelopeOf(cap, body) {
1279
+ if (cap.name === "yes-no") return body.statement === void 0 ? "results" : null;
1280
+ if (cap.name === "answer") return body.question === void 0 ? "results" : null;
1281
+ return cap.envelope;
1282
+ }
1283
+ function client(io, v) {
1284
+ if (!(v.key ?? io.env.MS_API_KEY)) throw new UsageError("no API key. Run export MS_API_KEY=sk-ms-..., or pass --key. Get a key at https://console.milliseconds.ai.");
1285
+ return new DecisionMachine({
1286
+ apiKey: v.key ?? io.env.MS_API_KEY,
1287
+ ...v["base-url"] === void 0 ? {} : { baseUrl: v["base-url"] },
1288
+ ...v.retries === void 0 ? {} : { maxRetries: number("--retries", v.retries) },
1289
+ ...v.timeout === void 0 ? {} : { timeout: number("--timeout", v.timeout) },
1290
+ ...io.fetch ? { fetch: io.fetch } : {}
1291
+ });
1292
+ }
1293
+ /** stdout holds results only. One blank line separates the texts of a batch. */
1294
+ function emit(io, decided, mode, cap, color, batch, bare, names) {
1295
+ if (decided.length === 0) return;
1296
+ const dim = dimmer(color);
1297
+ const indent = io.stdoutIsTty ? 2 : 0;
1298
+ if (mode === "raw") {
1299
+ const raws = decided.map((d) => d.raw);
1300
+ io.out(JSON.stringify(batch ? { results: raws } : raws[0], null, indent));
1301
+ return;
1302
+ }
1303
+ if (mode === "json") {
1304
+ const results = decided.map((d) => d.result);
1305
+ io.out(JSON.stringify(batch ? results : results[0], null, indent));
1306
+ return;
1307
+ }
1308
+ if (mode === "jsonl") {
1309
+ for (const d of decided) io.out(JSON.stringify(line(d, cap.envelope)));
1310
+ return;
1311
+ }
1312
+ if (mode === "quiet") {
1313
+ for (const d of decided) for (const l of cap.quiet(d.result)) io.out(l);
1314
+ return;
1315
+ }
1316
+ const blocks = decided.map((d) => batch ? `${dim(head(d.text))}\n${cap.table(d.result, dim, names)}` : cap.table(d.result, dim, names));
1317
+ io.out(blocks.join("\n\n"));
1318
+ if (bare !== void 0 && cap.name === "classify") io.out(`\n Bare label names score worse. Try: ${bare}="charges and refunds"`);
1319
+ }
1320
+ const head = (text) => text.length > 72 ? `${text.slice(0, 71)}…` : text;
1321
+ /** One compact object per line, with the input text, so a batch stays joinable. */
1322
+ function line(d, env) {
1323
+ if (Array.isArray(d.result)) return {
1324
+ text: d.text,
1325
+ [env ?? "results"]: d.result
1326
+ };
1327
+ return {
1328
+ ...d.result,
1329
+ text: d.text
1330
+ };
1331
+ }
1332
+ function usageLine(t) {
1333
+ const rate = t.rateLimit ? `, ${int(t.rateLimit.remainingRequests)}/${int(t.rateLimit.limitRequests)} requests left` : "";
1334
+ return `usage: ${int(t.chars)} chars, ${int(t.tokens)} tokens, ${int(t.ms)} ms inference${rate}`;
1335
+ }
1336
+ /** `--check`, `--min` and `--min-confidence`. The result still printed, so this only exits 3. */
1337
+ function failed(decided, v, min, minConfidence, cap) {
1338
+ if (v.check !== true && min === null && minConfidence === null) return false;
1339
+ for (const d of decided) {
1340
+ const list = items(d.result);
1341
+ if (v.check === true && list.length === 0) return true;
1342
+ for (const item of list) {
1343
+ if (v.check === true) {
1344
+ if (!(cap.name === "verify" ? item.matches === true : item.answer === true)) return true;
1345
+ }
1346
+ if (min !== null && typeof item.probability === "number" && item.probability < min) return true;
1347
+ if (minConfidence !== null && typeof item.confidence === "number" && item.confidence < minConfidence) return true;
1348
+ }
1349
+ }
1350
+ return false;
1351
+ }
1352
+ /** One small classify. It proves the key, the endpoint and the limits. */
1353
+ async function check(io, dm, v, color) {
1354
+ const started = Date.now();
1355
+ const { usage } = await dm.classify("ok", ["yes", "no"]).withUsage();
1356
+ const round = Date.now() - started;
1357
+ const key = v.key ?? io.env.MS_API_KEY ?? "";
1358
+ const masked = key.length > 16 ? `${key.slice(0, 12)}…${key.slice(-4)}` : key;
1359
+ const rl = usage.rateLimit;
1360
+ if (v.json === true || !io.stdoutIsTty) {
1361
+ io.out(JSON.stringify({
1362
+ key: masked,
1363
+ endpoint: dm.baseUrl,
1364
+ round_trip_ms: round,
1365
+ inference_ms: usage.inferenceMs,
1366
+ rate_limit: rl
1367
+ }, null, io.stdoutIsTty ? 2 : 0));
1368
+ return 0;
1369
+ }
1370
+ const limits = rl ? `${int(rl.limitRequests)} req/min` : "unknown";
1371
+ const remaining = rl ? `${int(rl.remainingRequests)} req` : "unknown";
1372
+ const width = Math.max(limits.length, remaining.length);
1373
+ const dim = dimmer(color);
1374
+ io.out(kv([
1375
+ ["key", `${masked} valid`],
1376
+ ["endpoint", dm.baseUrl],
1377
+ ["latency", `${int(round)} ms round trip · ${int(usage.inferenceMs)} ms model`],
1378
+ ["limits", rl ? `${limits.padEnd(width)} · ${int(rl.limitTokens)} tok/min` : "unknown"],
1379
+ ["remaining", rl ? `${remaining.padEnd(width)} · ${int(rl.remainingTokens)} tok` : "unknown"]
1380
+ ], dim, 10));
1381
+ io.out("ok");
1382
+ return 0;
1383
+ }
1384
+ //#endregion
1385
+ //#region src/cli/index.ts
1386
+ /** All of stdin. A pipe ends it; a terminal ends it on Ctrl-D. */
1387
+ async function stdin() {
1388
+ const parts = [];
1389
+ for await (const part of process.stdin) parts.push(part);
1390
+ return Buffer.concat(parts).toString("utf8");
1391
+ }
1392
+ process.exitCode = await run({
1393
+ argv: process.argv.slice(2),
1394
+ env: process.env,
1395
+ out: (line) => process.stdout.write(`${line}\n`),
1396
+ err: (line) => process.stderr.write(`${line}\n`),
1397
+ stdin,
1398
+ stdinIsTty: process.stdin.isTTY === true,
1399
+ stdoutIsTty: process.stdout.isTTY === true
1400
+ });
1401
+ //#endregion
1402
+ export {};