@mafdet/cli 0.4.9 → 0.4.11

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +1 -1
  4. package/dist/agent/approvals.d.ts.map +1 -1
  5. package/dist/agent/approvals.js +8 -1
  6. package/dist/agent/loop.d.ts +13 -0
  7. package/dist/agent/loop.d.ts.map +1 -1
  8. package/dist/agent/loop.js +40 -16
  9. package/dist/agent/session.d.ts +2 -0
  10. package/dist/agent/session.d.ts.map +1 -1
  11. package/dist/agent/session.js +19 -4
  12. package/dist/api/account.d.ts.map +1 -1
  13. package/dist/api/account.js +46 -3
  14. package/dist/api/models.d.ts.map +1 -1
  15. package/dist/api/models.js +30 -4
  16. package/dist/api/sse-parser.d.ts.map +1 -1
  17. package/dist/api/sse-parser.js +9 -1
  18. package/dist/api/validate.d.ts +4 -0
  19. package/dist/api/validate.d.ts.map +1 -0
  20. package/dist/api/validate.js +15 -0
  21. package/dist/commands/account.d.ts.map +1 -1
  22. package/dist/commands/account.js +6 -5
  23. package/dist/commands/agent.d.ts.map +1 -1
  24. package/dist/commands/agent.js +6 -4
  25. package/dist/commands/chat.d.ts.map +1 -1
  26. package/dist/commands/chat.js +30 -5
  27. package/dist/commands/embed.d.ts.map +1 -1
  28. package/dist/commands/embed.js +2 -2
  29. package/dist/commands/models.js +4 -4
  30. package/dist/commands/runtime.d.ts +16 -1
  31. package/dist/commands/runtime.d.ts.map +1 -1
  32. package/dist/commands/runtime.js +25 -6
  33. package/dist/commands/speech.d.ts.map +1 -1
  34. package/dist/commands/speech.js +2 -2
  35. package/dist/output/money.d.ts +2 -0
  36. package/dist/output/money.d.ts.map +1 -1
  37. package/dist/output/money.js +4 -0
  38. package/dist/sessions/usage.d.ts.map +1 -1
  39. package/dist/sessions/usage.js +10 -1
  40. package/dist/tui/adapters/models.d.ts.map +1 -1
  41. package/dist/tui/adapters/models.js +2 -1
  42. package/dist/tui/tool-card.js +3 -3
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,66 @@ The version is the contract for the JSON output shapes and the exit codes.
6
6
  Being 0.x, semver still permits a break in a minor bump; the intent is not to
7
7
  use that latitude, and the promise becomes binding at 1.0.0.
8
8
 
9
+ ## 0.4.11 - 2026-08-12
10
+
11
+ A seventh audit round, fixed at the root rather than sink by sink - which is
12
+ what the first round asked for and what six rounds of patching individual
13
+ strings failed to do.
14
+
15
+ **One boundary for human-readable output.** Interactive agent failures,
16
+ model-controlled tool names and results, the command-line approval preview,
17
+ TUI tool cards, account tables and model retirement text all go through it
18
+ now. Previously each was a separate string that someone had to remember. The
19
+ approval preview mattered most: a control sequence there can redraw the diff
20
+ between what you are shown and what you are agreeing to.
21
+
22
+ **One definition of a price.** `isMicroUsd` is shared by the agent's cost
23
+ ceiling, chat estimates, `models show` and the TUI picker. A catalogue serving
24
+ `abc` used to crash `models show` and the model picker outright, and in chat it
25
+ threw _after_ the request had been paid for, losing the answer and the saved
26
+ session with it. Invalid prices now read as unknown, and `"0"` is still a
27
+ price.
28
+
29
+ **Remote JSON is validated where it arrives.** Catalogue, wallet, usage and
30
+ subscription responses are checked at the HTTP boundary instead of being
31
+ trusted because TypeScript describes them. Malformed token counts are ignored
32
+ rather than entering totals or BigInt arithmetic.
33
+
34
+ Machine output is unchanged throughout: `--json`, `--plain` and redirected
35
+ streams still carry exactly what the server sent.
36
+
37
+ ## 0.4.10 - 2026-08-12
38
+
39
+ A sixth audit round, and the finding that matters most is about how the last
40
+ one was tested.
41
+
42
+ **Four previously missed error paths now share one function.** 0.4.9 said every
43
+ human-readable error used it, which was still too broad: the interactive Agent
44
+ and other human-output paths were found separately and are fixed under
45
+ Unreleased. The four 0.4.10 paths were: `embed` and `speech` handled a surface
46
+ mismatch themselves, `mafdet agent` printed the upstream's message directly,
47
+ and the request id - which comes from a response header - was concatenated
48
+ outside the sanitiser inside the very function meant to be the boundary. All
49
+ four carried remote text to a terminal.
50
+
51
+ The test that was supposed to cover this called the sanitiser directly, so it
52
+ passed while every caller bypassed it. That is the same mistake as the one in
53
+ 0.4.7, one release later. The new tests go in at `embedCommand`,
54
+ `speechCommand` and `agentCommand` - the commands a user actually runs - and
55
+ each was checked to fail when its fix is removed.
56
+
57
+ **A cost ceiling refuses an unusable price before sending anything.** The
58
+ pre-flight check asked only whether the price fields were present, so `abc`,
59
+ `-1`, `1.5` and `" 2"` passed it and the run spent one request before the
60
+ post-response check refused the result. Both checks now share one definition,
61
+ which also fixes the opposite error: a price of `"0"` is a price, and a truthy
62
+ test was rejecting free models.
63
+
64
+ **An invalid token count no longer reaches the totals.** It had stopped
65
+ affecting cost, but a negative or fractional figure was still added to the
66
+ token summary and fed into the context estimate - producing negative totals
67
+ and steering a decision about what to send next.
68
+
9
69
  ## 0.4.9 - 2026-08-12
10
70
 
11
71
  A fifth audit round: two billing and terminal-safety fixes, and two smaller
@@ -21,6 +81,9 @@ present and valid; anything else is reported as unpriced, stops a run that
21
81
  asked for a ceiling, and keeps the answer that was generated.
22
82
 
23
83
  **Errors and diagnostics can no longer carry terminal control sequences.**
84
+ ⚠️ Not true as written: `doctor` and the shared reporter were fixed, but
85
+ `embed`, `speech`, the agent's summary and the request id still bypassed it.
86
+ Fixed in 0.4.10.
24
87
  `mafdet doctor` interpolated a model's `apiSurface` straight into its table,
25
88
  and a surface-mismatch error did the same into stderr. Rather than cleaning
26
89
  those two strings, the cleaning now happens at the two boundaries every
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The official Mafdet AI command line: model calls, account queries, and a
4
4
  permissioned local agent.
5
5
 
6
- > **0.4.9.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.11.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
7
7
  > advertises a command it cannot serve, so `mafdet help` always lists exactly
8
8
  > what works.
9
9
  >
@@ -23,8 +23,8 @@ Or build it from this repository:
23
23
 
24
24
  ```bash
25
25
  pnpm --filter @mafdet/cli build
26
- cd packages/cli && npm pack # -> mafdet-cli-0.4.9.tgz
27
- npm install -g ./mafdet-cli-0.4.9.tgz
26
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.11.tgz
27
+ npm install -g ./mafdet-cli-0.4.11.tgz
28
28
  ```
29
29
 
30
30
  ## The interactive session
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Mafdet AI 官方命令行:模型调用、账户查询,以及一个受权限约束的本地 Agent。
4
4
 
5
- > **0.4.9。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.11。** 英文版见 [README.md](./README.md)。
6
6
 
7
7
  ## 环境要求
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../src/agent/approvals.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,QAIrB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,QAIxB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GAAG,QAAQ,CAgBX;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACtC,aAAa,GAAE,OAAsC,GACpD,QAAQ,CAIV"}
1
+ {"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../src/agent/approvals.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,QAIrB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,QAIxB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC,GAAG,QAAQ,CAwBX;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACtC,aAAa,GAAE,OAAsC,GACpD,QAAQ,CAIV"}
@@ -1,4 +1,5 @@
1
1
  import { createInterface } from "node:readline/promises";
2
+ import { safeForTerminal } from "../output/terminal-safe.js";
2
3
  /**
3
4
  * `read-only`, and every non-interactive run that has not been given explicit
4
5
  * permission. Plan §8.1: a CI job must not acquire write access by accident,
@@ -24,7 +25,13 @@ export const approveAll = {
24
25
  export function interactiveApprover(io) {
25
26
  return {
26
27
  async approve(request) {
27
- io.stderr(`\n${request.summary}\n${request.preview}\n`);
28
+ // Always sanitised, unlike every other stderr write, and the exception
29
+ // is deliberate: this prompt only exists when a person is about to
30
+ // answer it, and the answer decides whether a file is written. A control
31
+ // sequence here could clear the screen or redraw the diff between what
32
+ // is shown and what is asked. There is no redirected-output case to
33
+ // preserve bytes for - a redirected run cannot answer the prompt.
34
+ io.stderr(safeForTerminal(`\n${request.summary}\n${request.preview}\n`, true));
28
35
  const rl = createInterface({
29
36
  input: io.input ?? process.stdin,
30
37
  output: io.output ?? process.stderr,
@@ -167,5 +167,18 @@ interface ToolCall {
167
167
  export declare function runAgent(options: AgentOptions): Promise<AgentReport>;
168
168
  /** Retry-After when the server gave one; otherwise exponential with jitter. */
169
169
  export declare function backoffMs(retryAfterSeconds: number | undefined, attempt: number, random?: () => number): number;
170
+ /**
171
+ * W-1 · Can this model be priced at all, before anything is sent?
172
+ *
173
+ * Exported and shared with the pre-flight check, because the two used to
174
+ * disagree: the check asked only whether the fields were truthy, so `abc`,
175
+ * `-1`, `1.5` and `" 2"` all passed it and the run spent one request before
176
+ * `canPrice` refused the result. A ceiling that cannot cover the first request
177
+ * is not the ceiling the user asked for.
178
+ *
179
+ * Note `"0"` is a valid price and truthy checks got that wrong in the other
180
+ * direction - a free model would have been refused.
181
+ */
182
+ export declare function hasValidPrices(model: CatalogModel | undefined): boolean;
170
183
  export {};
171
184
  //# sourceMappingURL=loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAE1B,iBAAiB;AACjB,eAAO,MAAM,cAAc;;;IAGzB,iFAAiF;;;qBAGnD,MAAM,GAAG,SAAS;CACjD,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,OAAO,cAAc,CAAC;AAEhD,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,WAAW,GACX,SAAS,GACT,UAAU,GACV,WAAW,GACX,cAAc;AAChB,8EAA8E;GAC5E,eAAe;AACjB;;;;GAIG;GACD,cAAc,GACd,OAAO,CAAC;AAEZ,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,UAAU,CAAC;IACvB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,uDAAuD;IACvD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChC;;;;;GAKG;GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC3D;;;;GAIG;GACD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;IAC/C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAYD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAiP1E;AAqID,+EAA+E;AAC/E,wBAAgB,SAAS,CACvB,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAOR;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAKvE"}
@@ -1,5 +1,6 @@
1
1
  import { ApiError } from "../api/errors.js";
2
2
  import { compactUntilItFits, requestSize, shouldCompact, } from "./context-budget.js";
3
+ import { isMicroUsd } from "../output/money.js";
3
4
  import { toolToJsonSchema, } from "./tools/types.js";
4
5
  /** Plan §7.4. */
5
6
  export const DEFAULT_LIMITS = {
@@ -54,11 +55,10 @@ export async function runAgent(options) {
54
55
  // discover it meant a run with an explicit ceiling still made one billable
55
56
  // request that could never be counted against it.
56
57
  if (limits.maxCostMicroUsd !== undefined &&
57
- (!options.pricing?.inputPricePer1mMicroUsd ||
58
- !options.pricing.outputPricePer1mMicroUsd)) {
59
- return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue publishes no price for ` +
60
- `${options.model}, so nothing can be counted towards the ceiling. ` +
61
- `Re-run without the limit to continue anyway.`);
58
+ !hasValidPrices(options.pricing)) {
59
+ return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue publishes no usable ` +
60
+ `price for ${options.model}, so nothing can be counted towards the ` +
61
+ `ceiling. Re-run without the limit to continue anyway.`);
62
62
  }
63
63
  for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
64
64
  if (options.signal?.aborted)
@@ -125,13 +125,24 @@ export async function runAgent(options) {
125
125
  }
126
126
  return stop(report, "error", error instanceof Error ? error.message : String(error));
127
127
  }
128
+ // W-3 · Only counts that survive validation reach the totals or the
129
+ // context state. A negative or fractional figure used to be added to the
130
+ // report and fed into the compaction ratio - it no longer priced anything,
131
+ // but it still produced negative token totals and steered a decision about
132
+ // what to send next.
128
133
  const usage = response.usage;
129
- if (usage?.prompt_tokens !== undefined) {
130
- lastPromptTokens = usage.prompt_tokens;
134
+ const promptTokens = isTokenCount(usage?.prompt_tokens)
135
+ ? usage.prompt_tokens
136
+ : undefined;
137
+ const completionTokens = isTokenCount(usage?.completion_tokens)
138
+ ? usage.completion_tokens
139
+ : undefined;
140
+ if (promptTokens !== undefined) {
141
+ lastPromptTokens = promptTokens;
131
142
  lastRequestSize = sentSize;
132
143
  }
133
- report.promptTokens += usage?.prompt_tokens ?? 0;
134
- report.completionTokens += usage?.completion_tokens ?? 0;
144
+ report.promptTokens += promptTokens ?? 0;
145
+ report.completionTokens += completionTokens ?? 0;
135
146
  // Cost comes from returned usage, never from the reservation: CLI-000B
136
147
  // showed the reserve cannot see the tool schema and under-counts badly.
137
148
  const turnCost = priceTurn(options.pricing, usage);
@@ -142,8 +153,10 @@ export async function runAgent(options) {
142
153
  ...(response.requestId === undefined
143
154
  ? {}
144
155
  : { requestId: response.requestId }),
145
- promptTokens: usage?.prompt_tokens ?? 0,
146
- completionTokens: usage?.completion_tokens ?? 0,
156
+ // W-3 · The validated values, so nothing downstream - the recorder, the
157
+ // session store, a JSON consumer - has to re-derive what was usable.
158
+ promptTokens: promptTokens ?? 0,
159
+ completionTokens: completionTokens ?? 0,
147
160
  costMicroUsd: turnCost,
148
161
  priced: canPrice(options.pricing, usage),
149
162
  });
@@ -333,9 +346,21 @@ export function backoffMs(retryAfterSeconds, attempt, random = Math.random) {
333
346
  function isTokenCount(value) {
334
347
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
335
348
  }
336
- /** Integer micro-USD, non-negative. Anything else is not a price. */
337
- function isPrice(value) {
338
- return typeof value === "string" && /^\d+$/.test(value);
349
+ /**
350
+ * W-1 · Can this model be priced at all, before anything is sent?
351
+ *
352
+ * Exported and shared with the pre-flight check, because the two used to
353
+ * disagree: the check asked only whether the fields were truthy, so `abc`,
354
+ * `-1`, `1.5` and `" 2"` all passed it and the run spent one request before
355
+ * `canPrice` refused the result. A ceiling that cannot cover the first request
356
+ * is not the ceiling the user asked for.
357
+ *
358
+ * Note `"0"` is a valid price and truthy checks got that wrong in the other
359
+ * direction - a free model would have been refused.
360
+ */
361
+ export function hasValidPrices(model) {
362
+ return (isMicroUsd(model?.inputPricePer1mMicroUsd) &&
363
+ isMicroUsd(model?.outputPricePer1mMicroUsd));
339
364
  }
340
365
  function canPrice(model, usage) {
341
366
  if (usage === undefined)
@@ -344,8 +369,7 @@ function canPrice(model, usage) {
344
369
  return false;
345
370
  if (!isTokenCount(usage.completion_tokens))
346
371
  return false;
347
- return (isPrice(model?.inputPricePer1mMicroUsd) &&
348
- isPrice(model?.outputPricePer1mMicroUsd));
372
+ return hasValidPrices(model);
349
373
  }
350
374
  function priceTurn(model, usage) {
351
375
  // V-1 · One gate, shared with the caller's reporting, so a turn can never be
@@ -44,6 +44,8 @@ export interface SessionOptions {
44
44
  onGoal?: (goal: string) => void;
45
45
  /** R-4 · False when stdout is a pipe or a file, which get exact bytes. */
46
46
  isTty?: boolean;
47
+ /** Whether stderr is a terminal; independent from stdout. */
48
+ isErrTty?: boolean;
47
49
  /**
48
50
  * A conversation to continue, rebuilt from a saved session (CLI-024).
49
51
  * Appended to in place, exactly as the fresh array would be.
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAwGzB;AAsHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAqHzB;AAsHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline/promises";
2
2
  import { isAgentEligible } from "../api/models.js";
3
3
  import { formatUsdWithUnpriced } from "../output/money.js";
4
4
  import { renderRecord } from "../output/format.js";
5
- import { safeForTerminal } from "../output/terminal-safe.js";
5
+ import { safeForTerminal, terminalSafeWriter, } from "../output/terminal-safe.js";
6
6
  import { runAgent } from "./loop.js";
7
7
  const HELP = `Session commands:
8
8
  /model [id] Show or switch the model
@@ -23,6 +23,18 @@ Anything else is sent to the model as the next goal.
23
23
  * rather than the last question.
24
24
  */
25
25
  export async function runSession(options) {
26
+ // Interactive session stderr is a human surface. Wrap it once so new slash
27
+ // commands and progress messages cannot accidentally bypass the boundary.
28
+ // Preserve a dynamic signal getter: the TUI swaps its AbortController after
29
+ // cancelling one request so the session can return to the prompt.
30
+ const signalDescriptor = Object.getOwnPropertyDescriptor(options, "signal");
31
+ options = {
32
+ ...options,
33
+ stderr: terminalSafeWriter(options.stderr, options.isErrTty ?? true),
34
+ };
35
+ if (signalDescriptor?.get !== undefined) {
36
+ Object.defineProperty(options, "signal", signalDescriptor);
37
+ }
26
38
  const history = options.history ?? [];
27
39
  const summary = {
28
40
  goals: 0,
@@ -105,7 +117,7 @@ export async function runSession(options) {
105
117
  options.stdout(`${safeForTerminal(report.finalText, options.isTty ?? true)}\n`);
106
118
  }
107
119
  if (report.stopReason !== "completed") {
108
- options.stderr(`[${report.stopReason}${report.error ? `: ${report.error}` : ""}]\n`);
120
+ humanError(options, `[${report.stopReason}${report.error ? `: ${report.error}` : ""}]\n`);
109
121
  }
110
122
  if (report.stopReason === "cancelled" && options.signal?.aborted)
111
123
  break;
@@ -296,12 +308,15 @@ function lineSource(input) {
296
308
  function reportEvent(event, options) {
297
309
  if (event.type === "tool") {
298
310
  const first = event.detail.split("\n")[0].slice(0, 100);
299
- options.stderr(` ${event.ok ? "-" : "!"} ${event.name}: ${first}\n`);
311
+ humanError(options, ` ${event.ok ? "-" : "!"} ${event.name}: ${first}\n`);
300
312
  }
301
313
  else if (event.type === "declined") {
302
- options.stderr(` declined: ${event.summary}\n`);
314
+ humanError(options, ` declined: ${event.summary}\n`);
303
315
  }
304
316
  else if (event.type === "rate_limited") {
305
317
  options.stderr(` rate limited, waiting ${event.waitMs}ms (attempt ${event.attempt})\n`);
306
318
  }
307
319
  }
320
+ function humanError(options, text) {
321
+ options.stderr(safeForTerminal(text, options.isErrTty ?? true));
322
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/api/account.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C;;;GAGG;AAEH,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE;QACX,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAyBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,yBAAyB,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
1
+ {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/api/account.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA6C5C;;;GAGG;AAEH,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE;QACX,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAyBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,yBAAyB,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
@@ -1,4 +1,47 @@
1
1
  import { ApiError } from "./errors.js";
2
+ import { z } from "zod";
3
+ import { parseApiData } from "./validate.js";
4
+ const integralMoney = z.string().regex(/^-?\d+$/);
5
+ const count = z.number().safe().int().nonnegative();
6
+ const walletSchema = z
7
+ .object({
8
+ balance_micro_usd: integralMoney,
9
+ bonus_micro_usd: integralMoney,
10
+ frozen_micro_usd: integralMoney,
11
+ available_micro_usd: integralMoney,
12
+ currency_anchor: z.string(),
13
+ })
14
+ .passthrough();
15
+ const usageSummarySchema = z
16
+ .object({
17
+ days: count,
18
+ from: z.string(),
19
+ to: z.string(),
20
+ count,
21
+ input_tokens: count,
22
+ output_tokens: count,
23
+ total_tokens: count,
24
+ charge_micro_usd: integralMoney,
25
+ })
26
+ .passthrough();
27
+ const subscriptionSchema = z
28
+ .object({
29
+ status: z.string(),
30
+ plan_slug: z.string().optional(),
31
+ plan_name: z.string().optional(),
32
+ current_period_end: z.string().optional(),
33
+ playground: z
34
+ .object({
35
+ allowance_micro_usd: integralMoney,
36
+ used_micro_usd: integralMoney,
37
+ remaining_micro_usd: integralMoney,
38
+ usage_percent: z.number().finite(),
39
+ state: z.string(),
40
+ })
41
+ .passthrough()
42
+ .optional(),
43
+ })
44
+ .passthrough();
2
45
  /**
3
46
  * The account plane answers 401 with its own error shape when the key lacks
4
47
  * `account:read`. That is a scope problem, not a bad key, and saying so saves
@@ -22,20 +65,20 @@ export async function fetchWallet(client) {
22
65
  const { data } = await client
23
66
  .getJson("/keyinfo/wallet")
24
67
  .catch(explainScope);
25
- return data;
68
+ return parseApiData(walletSchema, data, "wallet API");
26
69
  }
27
70
  export async function fetchUsage(client, days) {
28
71
  const query = days === undefined ? "" : `?days=${days}`;
29
72
  const { data } = await client
30
73
  .getJson(`/keyinfo/usage-summary${query}`)
31
74
  .catch(explainScope);
32
- return data;
75
+ return parseApiData(usageSummarySchema, data, "usage API");
33
76
  }
34
77
  export async function fetchSubscription(client) {
35
78
  const { data } = await client
36
79
  .getJson("/keyinfo/subscription")
37
80
  .catch(explainScope);
38
- return data;
81
+ return parseApiData(subscriptionSchema, data, "subscription API");
39
82
  }
40
83
  /** Hard stop on paging, so a busy key cannot turn one command into hundreds. */
41
84
  export const MAX_USAGE_EVENT_PAGES = 20;
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAUxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,SAAS,CA4BX;AAED,8CAA8C;AAC9C,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CAG3B;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,YAAY,GAAG,SAAS,CAY1B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBAFhB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM;CAO5B"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA6B5C,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAUxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,SAAS,CA4BX;AAED,8CAA8C;AAC9C,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CAK3B;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAKzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,YAAY,GAAG,SAAS,CAY1B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBAFhB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM;CAO5B"}
@@ -1,3 +1,29 @@
1
+ import { z } from "zod";
2
+ import { parseApiData } from "./validate.js";
3
+ const dataPlaneModelsSchema = z
4
+ .object({
5
+ data: z.array(z.object({ id: z.string() }).passthrough()).optional(),
6
+ })
7
+ .passthrough();
8
+ const catalogModelSchema = z
9
+ .object({
10
+ modelId: z.string(),
11
+ displayName: z.string().optional(),
12
+ provider: z.string().optional(),
13
+ capabilities: z.array(z.string()).optional(),
14
+ apiSurface: z.string().optional(),
15
+ contextLength: z.number().safe().int().nonnegative().optional(),
16
+ inputPricePer1mMicroUsd: z.string().optional(),
17
+ outputPricePer1mMicroUsd: z.string().optional(),
18
+ accessTier: z.string().optional(),
19
+ deprecated: z.boolean().optional(),
20
+ retires_at: z.string().nullable().optional(),
21
+ replacement_model_id: z.string().nullable().optional(),
22
+ })
23
+ .passthrough();
24
+ const catalogSchema = z
25
+ .object({ data: z.array(catalogModelSchema).optional() })
26
+ .passthrough();
1
27
  /**
2
28
  * Agent admission. Every condition is something the catalogue states outright;
3
29
  * nothing is inferred from the model id, the provider or the display name —
@@ -59,13 +85,13 @@ export function intersectModels(dataPlane, catalog) {
59
85
  }
60
86
  /** GET /v1/models — the key's access list. */
61
87
  export async function fetchAccessibleModels(client) {
62
- const { data } = await client.getJson("/models");
63
- return data.data ?? [];
88
+ const response = await client.getJson("/models");
89
+ return (parseApiData(dataPlaneModelsSchema, response.data, "model API").data ?? []);
64
90
  }
65
91
  /** GET /models on the business API — public catalogue, no key required. */
66
92
  export async function fetchCatalog(client) {
67
- const { data } = await client.getJson("/models");
68
- return data.data ?? [];
93
+ const response = await client.getJson("/models");
94
+ return (parseApiData(catalogSchema, response.data, "model catalogue").data ?? []);
69
95
  }
70
96
  /**
71
97
  * U-3 · Is this a usable model identifier?
@@ -1 +1 @@
1
- {"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,+EAA+E;AAC/E,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAMjC,kDAAkD;IAClD,GAAG,IAAI,MAAM,EAAE;IASf,OAAO,CAAC,KAAK;CAYd;AAED,8DAA8D;AAC9D,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAS;gBAEQ,MAAM,GAAE,YAAiB;IAEtD,4DAA4D;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAgD9B,MAAM,IAAI,YAAY;CASvB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AACzD,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,GAAE,YAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,GAAE,YAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CAqGvB"}
1
+ {"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,+EAA+E;AAC/E,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAMjC,kDAAkD;IAClD,GAAG,IAAI,MAAM,EAAE;IASf,OAAO,CAAC,KAAK;CAYd;AAED,8DAA8D;AAC9D,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAS;gBAEQ,MAAM,GAAE,YAAiB;IAEtD,4DAA4D;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAgD9B,MAAM,IAAI,YAAY;CASvB;AAaD,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AACzD,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,GAAE,YAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,GAAE,YAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CAqGvB"}
@@ -61,7 +61,7 @@ export class ChatStreamAssembler {
61
61
  }
62
62
  // Read usage wherever it appears — it rides a frame that still has a
63
63
  // (empty-delta) choice on this gateway.
64
- if (chunk.usage)
64
+ if (isStreamUsage(chunk.usage))
65
65
  this.usage = chunk.usage;
66
66
  const choice = chunk.choices?.[0];
67
67
  if (!choice)
@@ -100,6 +100,14 @@ export class ChatStreamAssembler {
100
100
  };
101
101
  }
102
102
  }
103
+ function isStreamUsage(value) {
104
+ if (value === null || typeof value !== "object")
105
+ return false;
106
+ const usage = value;
107
+ return ["prompt_tokens", "completion_tokens", "total_tokens"].every((key) => typeof usage[key] === "number" &&
108
+ Number.isSafeInteger(usage[key]) &&
109
+ usage[key] >= 0);
110
+ }
103
111
  export const DEFAULT_STREAM_IDLE_MS = 120_000;
104
112
  export const DEFAULT_STREAM_MAX_BYTES = 32 * 1024 * 1024;
105
113
  /** Matches the agent's wall-clock ceiling (§7.4), for the same reason. */
@@ -0,0 +1,4 @@
1
+ import type { z } from "zod";
2
+ /** Validate untrusted JSON at the HTTP boundary and expose a stable CLI error. */
3
+ export declare function parseApiData<T>(schema: z.ZodType<T>, value: unknown, label: string): T;
4
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/api/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAK7B,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,CAAC,EAC5B,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,GACZ,CAAC,CAUH"}
@@ -0,0 +1,15 @@
1
+ import { ExitCode } from "../output/exit-codes.js";
2
+ import { ApiError } from "./errors.js";
3
+ /** Validate untrusted JSON at the HTTP boundary and expose a stable CLI error. */
4
+ export function parseApiData(schema, value, label) {
5
+ const parsed = schema.safeParse(value);
6
+ if (parsed.success)
7
+ return parsed.data;
8
+ const issue = parsed.error.issues[0];
9
+ const path = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
10
+ throw new ApiError({
11
+ kind: "server",
12
+ exitCode: ExitCode.Unavailable,
13
+ message: `${label} returned an invalid response${path}`,
14
+ });
15
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAOnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAUtB,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAmGnB"}
1
+ {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAQnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAUtB,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CA8GnB"}
@@ -2,6 +2,7 @@ import { fetchSubscription, fetchUsage, fetchWallet } from "../api/account.js";
2
2
  import { ExitCode } from "../output/exit-codes.js";
3
3
  import { parseFormatFlags, renderJson, renderRecord, } from "../output/format.js";
4
4
  import { formatUsd } from "../output/money.js";
5
+ import { safeForTerminal } from "../output/terminal-safe.js";
5
6
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
6
7
  const USAGE = `Usage:
7
8
  mafdet account balance [--json]
@@ -30,12 +31,12 @@ export async function accountCommand(argv, io) {
30
31
  io.stdout(renderJson(wallet));
31
32
  return ExitCode.Ok;
32
33
  }
33
- io.stdout(renderRecord([
34
+ io.stdout(safeForTerminal(renderRecord([
34
35
  ["balance", formatUsd(wallet.balance_micro_usd)],
35
36
  ["bonus", formatUsd(wallet.bonus_micro_usd)],
36
37
  ["frozen", formatUsd(wallet.frozen_micro_usd)],
37
38
  ["available", formatUsd(wallet.available_micro_usd)],
38
- ]));
39
+ ]), io.isTty ?? process.stdout.isTTY === true));
39
40
  return ExitCode.Ok;
40
41
  }
41
42
  if (subcommand === "usage") {
@@ -57,7 +58,7 @@ export async function accountCommand(argv, io) {
57
58
  io.stdout(renderJson(usage));
58
59
  return ExitCode.Ok;
59
60
  }
60
- io.stdout(renderRecord([
61
+ io.stdout(safeForTerminal(renderRecord([
61
62
  ["window", `${usage.days} day(s), ${usage.from} -> ${usage.to}`],
62
63
  // Throttled attempts land in the ledger with a zero charge
63
64
  // (CLI-001), so the count and the money answer different questions.
@@ -68,7 +69,7 @@ export async function accountCommand(argv, io) {
68
69
  ["input tokens", String(usage.input_tokens)],
69
70
  ["output tokens", String(usage.output_tokens)],
70
71
  ["charged", formatUsd(usage.charge_micro_usd)],
71
- ]));
72
+ ]), io.isTty ?? process.stdout.isTTY === true));
72
73
  return ExitCode.Ok;
73
74
  }
74
75
  if (args.length > 0)
@@ -90,7 +91,7 @@ export async function accountCommand(argv, io) {
90
91
  `${formatUsd(pg.used_micro_usd)} (${pg.usage_percent}%)`,
91
92
  ], ["playground left", formatUsd(pg.remaining_micro_usd)]);
92
93
  }
93
- io.stdout(renderRecord(rows));
94
+ io.stdout(safeForTerminal(renderRecord(rows), io.isTty ?? process.stdout.isTTY === true));
94
95
  return ExitCode.Ok;
95
96
  }
96
97
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAiBvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAmBtB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;IACpD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC9C;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CAuInB;AAoLD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAiBvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAmBtB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;IACpD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC9C;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA2InB;AAoLD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
@@ -17,7 +17,7 @@ import { Workspace, WorkspaceError } from "../filesystem/workspace.js";
17
17
  import { ExitCode } from "../output/exit-codes.js";
18
18
  import { renderJson, renderRecord } from "../output/format.js";
19
19
  import { formatUsdWithUnpriced } from "../output/money.js";
20
- import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
20
+ import { buildRuntime, reportError, writeHumanError, requireApiKey, } from "./runtime.js";
21
21
  const USAGE = `Usage:
22
22
  mafdet agent "<goal>" [--model <id>] [--allow-write] [--read-only]
23
23
  [--allow-command "<prefix>"]... [--max-turns <n>]
@@ -82,7 +82,7 @@ export async function agentCommand(argv, io) {
82
82
  if (parsed.allowWrite && !interactive) {
83
83
  io.stderr("note: --allow-write needs an interactive terminal to confirm changes; running read-only\n");
84
84
  }
85
- io.stderr(`workspace: ${workspace.root}\nmodel: ${model} (${mode})\n`);
85
+ writeHumanError(io, `workspace: ${workspace.root}\nmodel: ${model} (${mode})\n`);
86
86
  if (parsed.goal.length === 0) {
87
87
  if (!interactive) {
88
88
  io.stderr(`mafdet agent: no goal\n${USAGE}`);
@@ -119,6 +119,7 @@ export async function agentCommand(argv, io) {
119
119
  stderr: io.stderr,
120
120
  signal: io.signal,
121
121
  isTty: isTty(io),
122
+ isErrTty: io.isErrTty ?? process.stderr.isTTY === true,
122
123
  ...(io.readLine === undefined ? {} : { readLine: io.readLine }),
123
124
  });
124
125
  }
@@ -345,7 +346,7 @@ function reportEvent(event, io) {
345
346
  io.stderr(`\n[turn ${event.turn}]\n`);
346
347
  else if (event.type === "tool") {
347
348
  const first = event.detail.split("\n")[0].slice(0, 100);
348
- io.stderr(` ${event.ok ? "-" : "!"} ${event.name}: ${first}\n`);
349
+ writeHumanError(io, ` ${event.ok ? "-" : "!"} ${event.name}: ${first}\n`);
349
350
  }
350
351
  else if (event.type === "rate_limited") {
351
352
  io.stderr(` rate limited, waiting ${event.waitMs}ms (attempt ${event.attempt})\n`);
@@ -383,8 +384,9 @@ function finish(report, json, io) {
383
384
  `~${formatUsdWithUnpriced(report.costMicroUsd, report.unpricedRequests)}`,
384
385
  ],
385
386
  ]));
387
+ // W-2 · `report.error` is the upstream's message, verbatim.
386
388
  if (report.error)
387
- io.stderr(`error: ${report.error}\n`);
389
+ writeHumanError(io, `error: ${report.error}\n`);
388
390
  }
389
391
  switch (report.stopReason) {
390
392
  case "completed":
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AA8SD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CASpB"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAgTD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CAYpB"}
@@ -9,7 +9,7 @@ import { upsertIndexEntry } from "../sessions/index-file.js";
9
9
  import { decideRecording } from "../sessions/policy.js";
10
10
  import { SessionWriter } from "../sessions/store.js";
11
11
  import { ExitCode } from "../output/exit-codes.js";
12
- import { formatUsd } from "../output/money.js";
12
+ import { formatUsd, isMicroUsd } from "../output/money.js";
13
13
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
14
14
  const USAGE = `Usage:
15
15
  mafdet chat [prompt] [--model <id>] [--system <file>] [--input <file>]
@@ -276,11 +276,13 @@ async function streamChat(runtime, body, args, io) {
276
276
  }
277
277
  async function completeChat(runtime, body, args, io) {
278
278
  const { data, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
279
- const answer = data.choices?.[0]?.message?.content ?? "";
279
+ const rawAnswer = data.choices?.[0]?.message?.content;
280
+ const answer = typeof rawAnswer === "string" ? rawAnswer : "";
281
+ const usage = validUsage(data.usage);
280
282
  io.stdout(`${safeForTerminal(answer, isTty(io))}\n`);
281
283
  const model = await priceModel(runtime, args.model, !args.quiet || args.save);
282
- summarise(args, data.usage, io, model);
283
- saveChat(args, io, answer, data.usage, requestId, model);
284
+ summarise(args, usage, io, model);
285
+ saveChat(args, io, answer, usage, requestId, model);
284
286
  return ExitCode.Ok;
285
287
  }
286
288
  /**
@@ -300,9 +302,32 @@ function summarise(args, usage, io, model, options = {}) {
300
302
  io.stderr(`${prefix}${usage.prompt_tokens} in / ${usage.completion_tokens} out tokens${cost}\n`);
301
303
  }
302
304
  export function estimateChargeMicroUsd(model, usage) {
303
- if (!model?.inputPricePer1mMicroUsd || !model.outputPricePer1mMicroUsd)
305
+ if (!isMicroUsd(model?.inputPricePer1mMicroUsd) ||
306
+ !isMicroUsd(model.outputPricePer1mMicroUsd))
304
307
  return undefined;
305
308
  const ceilDiv = (tokens, pricePer1m) => (BigInt(tokens) * BigInt(pricePer1m) + 999999n) / 1000000n;
306
309
  return (ceilDiv(usage.prompt_tokens, model.inputPricePer1mMicroUsd) +
307
310
  ceilDiv(usage.completion_tokens, model.outputPricePer1mMicroUsd));
308
311
  }
312
+ function validUsage(value) {
313
+ if (value === null || typeof value !== "object")
314
+ return undefined;
315
+ const usage = value;
316
+ const count = (field) => {
317
+ const candidate = usage[field];
318
+ return typeof candidate === "number" &&
319
+ Number.isSafeInteger(candidate) &&
320
+ candidate >= 0
321
+ ? candidate
322
+ : undefined;
323
+ };
324
+ const prompt = count("prompt_tokens");
325
+ const completion = count("completion_tokens");
326
+ if (prompt === undefined || completion === undefined)
327
+ return undefined;
328
+ return {
329
+ prompt_tokens: prompt,
330
+ completion_tokens: completion,
331
+ total_tokens: count("total_tokens") ?? prompt + completion,
332
+ };
333
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/commands/embed.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAQtB,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA8DnB"}
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/commands/embed.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAQtB,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA8DnB"}
@@ -3,7 +3,7 @@ import { ApiError } from "../api/errors.js";
3
3
  import { assertSurface, fetchCatalog, SurfaceMismatchError, } from "../api/models.js";
4
4
  import { ExitCode } from "../output/exit-codes.js";
5
5
  import { writeOutputFile } from "../output/write-file.js";
6
- import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
6
+ import { buildRuntime, reportError, writeHumanError, requireApiKey, } from "./runtime.js";
7
7
  const USAGE = `Usage:
8
8
  mafdet embed [text | <file>] --model <id> [--output <file>] [--force]
9
9
 
@@ -62,7 +62,7 @@ export async function embedCommand(argv, io) {
62
62
  }
63
63
  catch (error) {
64
64
  if (error instanceof SurfaceMismatchError) {
65
- io.stderr(`mafdet embed: ${error.message}\n`);
65
+ writeHumanError(io, `mafdet embed: ${error.message}\n`);
66
66
  return ExitCode.Usage;
67
67
  }
68
68
  return reportError(error, io);
@@ -1,7 +1,7 @@
1
1
  import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/models.js";
2
2
  import { ExitCode } from "../output/exit-codes.js";
3
3
  import { parseFormatFlags, renderJson, renderPlain, renderRecord, renderTable, } from "../output/format.js";
4
- import { formatUsd } from "../output/money.js";
4
+ import { formatUsd, isMicroUsd } from "../output/money.js";
5
5
  import { safeForTerminal } from "../output/terminal-safe.js";
6
6
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
7
7
  const USAGE = `Usage:
@@ -151,20 +151,20 @@ function showModel(sets, args, format, io) {
151
151
  ["agent eligible", model.agentEligible ? "yes" : "no"],
152
152
  [
153
153
  "input / 1M",
154
- model.inputPricePer1mMicroUsd
154
+ isMicroUsd(model.inputPricePer1mMicroUsd)
155
155
  ? formatUsd(model.inputPricePer1mMicroUsd)
156
156
  : "-",
157
157
  ],
158
158
  [
159
159
  "output / 1M",
160
- model.outputPricePer1mMicroUsd
160
+ isMicroUsd(model.outputPricePer1mMicroUsd)
161
161
  ? formatUsd(model.outputPricePer1mMicroUsd)
162
162
  : "-",
163
163
  ],
164
164
  [
165
165
  "deprecated",
166
166
  model.deprecated
167
- ? `yes${model.retires_at ? ` (retires ${model.retires_at})` : ""}`
167
+ ? clean(`yes${model.retires_at ? ` (retires ${model.retires_at})` : ""}`)
168
168
  : "no",
169
169
  ],
170
170
  ]));
@@ -38,7 +38,22 @@ export declare function requireApiKey(config: LoadedConfig): void;
38
38
  * prints a raw upstream message: ApiError redacted it at construction.
39
39
  */
40
40
  /**
41
- * V-2 · The one place errors reach a person.
41
+ * W-2 · The one function every human-readable stderr write goes through.
42
+ *
43
+ * `reportError` was described as that and was not: `embed` and `speech`
44
+ * handled SurfaceMismatchError themselves, the agent's summary printed
45
+ * `report.error` directly, and even inside `reportError` the request id was
46
+ * concatenated outside the sanitiser. Three real command paths and one field,
47
+ * all carrying remote text to a terminal.
48
+ *
49
+ * The test that was supposed to cover this called `reportError` directly -
50
+ * the callee - and so passed while every caller bypassed it. That mistake has
51
+ * now been made twice; the way out is a function that is easier to use than
52
+ * `io.stderr` is, so there is nothing to remember.
53
+ */
54
+ export declare function writeHumanError(io: CommandIo, text: string): void;
55
+ /**
56
+ * V-2 · The one place *thrown* errors reach a person.
42
57
  *
43
58
  * Every command funnels through here, so this is where untrusted text in an
44
59
  * error message stops being able to drive the terminal. Patching the
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,sCAAsC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,yCAAyC;IACzC,YAAY,EAAE,UAAU,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAgBnD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CASxD;AAED;;;GAGG;AACH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAqBnE"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,sCAAsC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,yCAAyC;IACzC,YAAY,EAAE,UAAU,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAgBnD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CASxD;AAED;;;GAGG;AACH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAqBnE"}
@@ -39,7 +39,24 @@ export function requireApiKey(config) {
39
39
  * prints a raw upstream message: ApiError redacted it at construction.
40
40
  */
41
41
  /**
42
- * V-2 · The one place errors reach a person.
42
+ * W-2 · The one function every human-readable stderr write goes through.
43
+ *
44
+ * `reportError` was described as that and was not: `embed` and `speech`
45
+ * handled SurfaceMismatchError themselves, the agent's summary printed
46
+ * `report.error` directly, and even inside `reportError` the request id was
47
+ * concatenated outside the sanitiser. Three real command paths and one field,
48
+ * all carrying remote text to a terminal.
49
+ *
50
+ * The test that was supposed to cover this called `reportError` directly -
51
+ * the callee - and so passed while every caller bypassed it. That mistake has
52
+ * now been made twice; the way out is a function that is easier to use than
53
+ * `io.stderr` is, so there is nothing to remember.
54
+ */
55
+ export function writeHumanError(io, text) {
56
+ io.stderr(safeForTerminal(text, io.isErrTty ?? process.stderr.isTTY === true));
57
+ }
58
+ /**
59
+ * V-2 · The one place *thrown* errors reach a person.
43
60
  *
44
61
  * Every command funnels through here, so this is where untrusted text in an
45
62
  * error message stops being able to drive the terminal. Patching the
@@ -49,20 +66,22 @@ export function requireApiKey(config) {
49
66
  * Redirected stderr keeps the bytes, the same rule as everywhere else.
50
67
  */
51
68
  export function reportError(error, io) {
52
- const safe = (text) => safeForTerminal(text, io.isErrTty ?? process.stderr.isTTY === true);
53
69
  if (error instanceof ApiError) {
70
+ // W-2 · The request id comes from a response header
71
+ // (`x-litellm-call-id`), so it is remote text too - it used to be
72
+ // concatenated outside the sanitiser.
54
73
  const id = error.requestId ? ` (request ${error.requestId})` : "";
55
- io.stderr(`mafdet: ${safe(error.message)}${id}\n`);
74
+ writeHumanError(io, `mafdet: ${error.message}${id}\n`);
56
75
  if (error.kind === "rate_limited" &&
57
76
  error.retryAfterSeconds !== undefined) {
58
- io.stderr(`mafdet: retry after ${error.retryAfterSeconds}s\n`);
77
+ writeHumanError(io, `mafdet: retry after ${error.retryAfterSeconds}s\n`);
59
78
  }
60
79
  return error.exitCode;
61
80
  }
62
81
  if (error instanceof ConfigError) {
63
- io.stderr(`mafdet: ${safe(error.message)}\n`);
82
+ writeHumanError(io, `mafdet: ${error.message}\n`);
64
83
  return ExitCode.Usage;
65
84
  }
66
- io.stderr(`mafdet: ${safe(error.message)}\n`);
85
+ writeHumanError(io, `mafdet: ${error.message}\n`);
67
86
  return ExitCode.General;
68
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"speech.d.ts","sourceRoot":"","sources":["../../src/commands/speech.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAStB,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,QAAQ,CAAC,CAyEnB"}
1
+ {"version":3,"file":"speech.d.ts","sourceRoot":"","sources":["../../src/commands/speech.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAStB,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,QAAQ,CAAC,CAyEnB"}
@@ -3,7 +3,7 @@ import { ApiError } from "../api/errors.js";
3
3
  import { assertSurface, fetchCatalog, SurfaceMismatchError, } from "../api/models.js";
4
4
  import { ExitCode } from "../output/exit-codes.js";
5
5
  import { writeOutputFile } from "../output/write-file.js";
6
- import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
6
+ import { buildRuntime, reportError, writeHumanError, requireApiKey, } from "./runtime.js";
7
7
  const USAGE = `Usage:
8
8
  mafdet speech [text | <file>] --model <id> --output <file> [--voice <name>] [--force]
9
9
 
@@ -69,7 +69,7 @@ export async function speechCommand(argv, io) {
69
69
  }
70
70
  catch (error) {
71
71
  if (error instanceof SurfaceMismatchError) {
72
- io.stderr(`mafdet speech: ${error.message}\n`);
72
+ writeHumanError(io, `mafdet speech: ${error.message}\n`);
73
73
  return ExitCode.Usage;
74
74
  }
75
75
  // The gateway requires a voice for TTS and says so with a code; turn that
@@ -5,6 +5,8 @@
5
5
  * reason: a float round-trip can turn a balance into a number that does not
6
6
  * match the invoice. Everything here is BigInt in, string out.
7
7
  */
8
+ /** A non-negative integral micro-USD value as spoken by Mafdet APIs. */
9
+ export declare function isMicroUsd(value: unknown): value is string;
8
10
  export declare function parseMicroUsd(value: string | number | bigint): bigint;
9
11
  /**
10
12
  * Render micro-USD as dollars. `minimumFractionDigits` keeps small amounts
@@ -1 +1 @@
1
- {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/output/money.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAClC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3C,MAAM,CAeR;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAIR"}
1
+ {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/output/money.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE1D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAClC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3C,MAAM,CAeR;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAIR"}
@@ -6,6 +6,10 @@
6
6
  * match the invoice. Everything here is BigInt in, string out.
7
7
  */
8
8
  const MICROS_PER_USD = 1000000n;
9
+ /** A non-negative integral micro-USD value as spoken by Mafdet APIs. */
10
+ export function isMicroUsd(value) {
11
+ return typeof value === "string" && /^\d+$/.test(value);
12
+ }
9
13
  export function parseMicroUsd(value) {
10
14
  if (typeof value === "bigint")
11
15
  return value;
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/sessions/usage.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;CAC3E;AAoGD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,CA6D7E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAcrE"}
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/sessions/usage.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;CAC3E;AA6GD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,CA6D7E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAcrE"}
@@ -76,8 +76,17 @@ function settlementsIn(events) {
76
76
  }
77
77
  return found;
78
78
  }
79
+ /**
80
+ * W-3 · A token count is a non-negative whole number, or it is not a count.
81
+ *
82
+ * `Number.isFinite` accepted -5 and 1.5, so a transcript written by an older
83
+ * build - or by a gateway having a bad day - could produce a negative or
84
+ * fractional total in `sessions usage`.
85
+ */
79
86
  function count(value) {
80
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
87
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
88
+ ? value
89
+ : 0;
81
90
  }
82
91
  /** Was this request priced at all? Absent is not the same as zero. */
83
92
  function isPriced(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/tui/adapters/models.ts"],"names":[],"mappings":"AAiBA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAKzE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB,GAAG,OAAO,CAAC,cAAc,CAAC,CAoB1B;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAoB1D;AASD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,EAAE,MAAM,GACZ,SAAS,aAAa,EAAE,CAgB1B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,UAAU,CA8Cb"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/tui/adapters/models.ts"],"names":[],"mappings":"AAiBA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAMzE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB,GAAG,OAAO,CAAC,cAAc,CAAC,CAoB1B;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAoB1D;AASD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,EAAE,MAAM,GACZ,SAAS,aAAa,EAAE,CAgB1B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,UAAU,CA8Cb"}
@@ -13,6 +13,7 @@ import { buildRuntime } from "../../commands/runtime.js";
13
13
  import { parseUserConfig } from "../../config/config.js";
14
14
  import { userConfigPath } from "../../config/paths.js";
15
15
  import { formatUsd } from "../../output/money.js";
16
+ import { isMicroUsd } from "../../output/money.js";
16
17
  /**
17
18
  * The catalogue does not publish a per-route RPM - checked against the live
18
19
  * `/models` response, which has 31 models and no rate field at all. Plan §19.5
@@ -62,7 +63,7 @@ export function describeModel(model) {
62
63
  function priceSummary(model) {
63
64
  const input = model.inputPricePer1mMicroUsd;
64
65
  const output = model.outputPricePer1mMicroUsd;
65
- if (input === undefined || output === undefined)
66
+ if (!isMicroUsd(input) || !isMicroUsd(output))
66
67
  return undefined;
67
68
  return `${formatUsd(input)}/${formatUsd(output)} per 1M`;
68
69
  }
@@ -122,11 +122,11 @@ function firstLine(text) {
122
122
  export function renderCard(card, capabilities) {
123
123
  const { dim, bold, accent } = palette(capabilities);
124
124
  const glyph = GLYPHS[card.state];
125
- const head = ` ${glyph} ${bold(card.name)} ${dim(card.arguments)}`;
125
+ const head = ` ${glyph} ${bold(stripAnsi(card.name))} ${dim(card.arguments)}`;
126
126
  if (card.detail === undefined || card.detail.length === 0)
127
127
  return `${head}\n`;
128
128
  const detail = card.state === "failed" || card.state === "refused"
129
- ? accent(card.detail)
130
- : dim(card.detail);
129
+ ? accent(firstLine(card.detail))
130
+ : dim(firstLine(card.detail));
131
131
  return `${head}\n ${detail}\n`;
132
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mafdet/cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Mafdet AI command line — model calls, account queries, and a permissioned local agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",