@mafdet/cli 0.4.10 → 0.4.12

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 (41) hide show
  1. package/CHANGELOG.md +52 -9
  2. package/README.md +8 -7
  3. package/README.zh-CN.md +5 -5
  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 +1 -9
  7. package/dist/agent/loop.d.ts.map +1 -1
  8. package/dist/agent/loop.js +16 -12
  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 +67 -5
  14. package/dist/api/chat-response.d.ts +32 -0
  15. package/dist/api/chat-response.d.ts.map +1 -0
  16. package/dist/api/chat-response.js +39 -0
  17. package/dist/api/models.d.ts.map +1 -1
  18. package/dist/api/models.js +30 -4
  19. package/dist/api/sse-parser.d.ts.map +1 -1
  20. package/dist/api/sse-parser.js +9 -1
  21. package/dist/api/validate.d.ts +4 -0
  22. package/dist/api/validate.d.ts.map +1 -0
  23. package/dist/api/validate.js +15 -0
  24. package/dist/commands/account.d.ts.map +1 -1
  25. package/dist/commands/account.js +6 -5
  26. package/dist/commands/agent.d.ts.map +1 -1
  27. package/dist/commands/agent.js +3 -2
  28. package/dist/commands/chat.d.ts.map +1 -1
  29. package/dist/commands/chat.js +35 -6
  30. package/dist/commands/models.js +4 -4
  31. package/dist/commands/sessions.d.ts.map +1 -1
  32. package/dist/commands/sessions.js +2 -1
  33. package/dist/output/money.d.ts +2 -0
  34. package/dist/output/money.d.ts.map +1 -1
  35. package/dist/output/money.js +4 -0
  36. package/dist/sessions/reconcile.d.ts.map +1 -1
  37. package/dist/sessions/reconcile.js +8 -6
  38. package/dist/tui/adapters/models.d.ts.map +1 -1
  39. package/dist/tui/adapters/models.js +2 -1
  40. package/dist/tui/tool-card.js +3 -3
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,17 +6,59 @@ 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.12 - 2026-08-12
10
+
11
+ - Validate usage-event pages, model identities, non-stream Chat responses and
12
+ Agent tool calls at the HTTP boundary. Malformed remote data now produces a
13
+ typed unavailable error instead of an empty success or a runtime exception.
14
+ - Require account and settled ledger amounts to be non-negative integer
15
+ micro-USD strings. An invalid settlement remains pending; it is never
16
+ rewritten as a fabricated zero-dollar settlement.
17
+ - Correct the reconciliation documentation: refresh queries the current key's
18
+ paged usage-event window and matches request IDs locally, while retaining
19
+ local estimates alongside settled figures.
20
+
21
+ ## 0.4.11 - 2026-08-12
22
+
23
+ A seventh audit round, fixed at the root rather than sink by sink - which is
24
+ what the first round asked for and what six rounds of patching individual
25
+ strings failed to do.
26
+
27
+ **One boundary for human-readable output.** Interactive agent failures,
28
+ model-controlled tool names and results, the command-line approval preview,
29
+ TUI tool cards, account tables and model retirement text all go through it
30
+ now. Previously each was a separate string that someone had to remember. The
31
+ approval preview mattered most: a control sequence there can redraw the diff
32
+ between what you are shown and what you are agreeing to.
33
+
34
+ **One definition of a price.** `isMicroUsd` is shared by the agent's cost
35
+ ceiling, chat estimates, `models show` and the TUI picker. A catalogue serving
36
+ `abc` used to crash `models show` and the model picker outright, and in chat it
37
+ threw _after_ the request had been paid for, losing the answer and the saved
38
+ session with it. Invalid prices now read as unknown, and `"0"` is still a
39
+ price.
40
+
41
+ **Remote JSON is validated where it arrives.** Catalogue, wallet, usage and
42
+ subscription responses are checked at the HTTP boundary instead of being
43
+ trusted because TypeScript describes them. Malformed token counts are ignored
44
+ rather than entering totals or BigInt arithmetic.
45
+
46
+ Machine output is unchanged throughout: `--json`, `--plain` and redirected
47
+ streams still carry exactly what the server sent.
48
+
9
49
  ## 0.4.10 - 2026-08-12
10
50
 
11
51
  A sixth audit round, and the finding that matters most is about how the last
12
52
  one was tested.
13
53
 
14
- **Every human-readable error now goes through one function.** 0.4.9 said this
15
- and it was not true: `embed` and `speech` handled a surface mismatch
16
- themselves, `mafdet agent` printed the upstream's message directly, and the
17
- request id - which comes from a response header - was concatenated outside the
18
- sanitiser inside the very function meant to be the boundary. Four paths, all
19
- carrying remote text to a terminal.
54
+ **Four previously missed error paths now share one function.** 0.4.9 said every
55
+ human-readable error used it, which was still too broad: the interactive Agent
56
+ and other human-output paths were found separately and are fixed under
57
+ Unreleased. The four 0.4.10 paths were: `embed` and `speech` handled a surface
58
+ mismatch themselves, `mafdet agent` printed the upstream's message directly,
59
+ and the request id - which comes from a response header - was concatenated
60
+ outside the sanitiser inside the very function meant to be the boundary. All
61
+ four carried remote text to a terminal.
20
62
 
21
63
  The test that was supposed to cover this called the sanitiser directly, so it
22
64
  passed while every caller bypassed it. That is the same mistake as the one in
@@ -313,9 +355,10 @@ each platform, and that deleting one is an ordinary file delete rather than a
313
355
  secure erase.
314
356
 
315
357
  `mafdet sessions usage <id> --refresh` asks the billing ledger what the
316
- requests actually cost and replaces the local estimate with the settled
317
- figure. It sends the request IDs already in the transcript and nothing else -
318
- no messages, no title, no workspace, no session ID. Requests paid for by a
358
+ requests actually cost and records the settled figure alongside the local
359
+ estimate. **Correction:** it queries the current key's paged usage-event
360
+ window; request IDs stay local for matching rather than being sent. No
361
+ messages, title, workspace or session ID leave the machine. Requests paid for by a
319
362
  different API key are left alone and reported rather than guessed at, and a
320
363
  request the ledger has not settled stays `pending` rather than being shown as
321
364
  zero.
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.10.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.12.** 中文版见 [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.10.tgz
27
- npm install -g ./mafdet-cli-0.4.10.tgz
26
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.12.tgz
27
+ npm install -g ./mafdet-cli-0.4.12.tgz
28
28
  ```
29
29
 
30
30
  ## The interactive session
@@ -87,7 +87,7 @@ prompt as an argument, or use `--input <file>`.
87
87
  | `mafdet speech [text\|<file>] --model <id> --output <file> --voice <name>` | Text to an audio file |
88
88
  | `mafdet account balance\|usage\|subscription` | Wallet, spend and plan (needs the `account:read` scope) |
89
89
  | `mafdet sessions list\|show\|usage\|delete` | Conversations saved on this computer (never calls a model) |
90
- | `mafdet sessions usage <id> --refresh` | Replace the local estimate with the billing ledger's figure |
90
+ | `mafdet sessions usage <id> --refresh` | Show settled ledger figures alongside the local estimate |
91
91
  | `mafdet config list` | The resolved settings and where each came from |
92
92
 
93
93
  `--json` on any query command emits one complete JSON document; `--plain`
@@ -247,9 +247,10 @@ erase — backups, snapshots and the filesystem itself may still hold copies.
247
247
  record. Those charges are estimates, computed with the gateway's own
248
248
  arithmetic but computed here.
249
249
 
250
- `--refresh` asks the billing ledger for the real figure. It sends the request
251
- IDs already in the transcript and nothing else — no messages, no title, no
252
- workspace, no session ID. Requests paid for by a different API key are left
250
+ `--refresh` asks the billing ledger for settled figures. It pages through the
251
+ current API key's usage events for the session's time window; request IDs stay
252
+ local and are used only to match returned rows. No messages, title, workspace
253
+ or session ID are sent. Requests paid for by a different API key are left
253
254
  alone and reported, so you know to run it again with that key.
254
255
 
255
256
  A request that the ledger has not caught up with stays `pending`, and a request
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Mafdet AI 官方命令行:模型调用、账户查询,以及一个受权限约束的本地 Agent。
4
4
 
5
- > **0.4.10。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.12。** 英文版见 [README.md](./README.md)。
6
6
 
7
7
  ## 环境要求
8
8
 
@@ -73,7 +73,7 @@ mafdet doctor
73
73
  | `mafdet agent`(不带目标,需终端) | 交互会话 |
74
74
  | `mafdet account balance\|usage\|subscription` | 钱包、用量、套餐(需 `account:read` scope) |
75
75
  | `mafdet sessions list\|show\|usage\|delete` | 保存在这台电脑上的会话(不会调用模型) |
76
- | `mafdet sessions usage <id> --refresh` | 用账单账本的权威金额替换本地估算 |
76
+ | `mafdet sessions usage <id> --refresh` | 在本地估算旁显示账单账本的已结算金额 |
77
77
  | `mafdet config list` | 当前生效的配置及其来源 |
78
78
 
79
79
  查询类命令都支持 `--json`(输出一份完整 JSON 文档)与 `--plain`(无表头的 TSV,方便
@@ -214,9 +214,9 @@ mafdet sessions delete <id> --yes
214
214
  `mafdet sessions usage <id>` 从本地记录累加一次会话的用量。那些金额是**估算**,
215
215
  算法与网关一致,但是在本地算的。
216
216
 
217
- `--refresh` 会去问账单账本要真实金额。它只发送存档里已有的 request ID,**别的什么都不发**——
218
- 不发消息、不发标题、不发工作区路径、不发 session ID。由另一把 API Key 支付的请求会被跳过
219
- 并如实告知,好让你知道该换那把 Key 再跑一次。
217
+ `--refresh` 会按会话时间窗口和游标查询当前 API Key 的用量事件;request ID 只留在本地,
218
+ 用于匹配账本返回的记录。它不发送消息、标题、工作区路径或 session ID。由另一把 API Key
219
+ 支付的请求会被跳过并如实告知,好让你知道该换那把 Key 再跑一次。
220
220
 
221
221
  账本还没跟上的请求保持 `pending`,从未拿到 ID 的请求保持 `unknown`。
222
222
  **两者都绝不显示为 `0`**——0 读起来像「免费」。
@@ -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,
@@ -1,3 +1,4 @@
1
+ import { type ToolCall } from "../api/chat-response.js";
1
2
  import type { HttpClient } from "../api/http.js";
2
3
  import type { CatalogModel } from "../api/models.js";
3
4
  import type { Approver } from "./approvals.js";
@@ -146,14 +147,6 @@ export interface ChatMessage {
146
147
  tool_calls?: ToolCall[];
147
148
  tool_call_id?: string;
148
149
  }
149
- interface ToolCall {
150
- id: string;
151
- type: "function";
152
- function: {
153
- name: string;
154
- arguments: string;
155
- };
156
- }
157
150
  /**
158
151
  * The read-only agent loop.
159
152
  *
@@ -180,5 +173,4 @@ export declare function backoffMs(retryAfterSeconds: number | undefined, attempt
180
173
  * direction - a free model would have been refused.
181
174
  */
182
175
  export declare function hasValidPrices(model: CatalogModel | undefined): boolean;
183
- export {};
184
176
  //# 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":"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"}
@@ -1,5 +1,7 @@
1
1
  import { ApiError } from "../api/errors.js";
2
+ import { parseChatCompletion, } from "../api/chat-response.js";
2
3
  import { compactUntilItFits, requestSize, shouldCompact, } from "./context-budget.js";
4
+ import { isMicroUsd } from "../output/money.js";
3
5
  import { toolToJsonSchema, } from "./tools/types.js";
4
6
  /** Plan §7.4. */
5
7
  export const DEFAULT_LIMITS = {
@@ -129,7 +131,7 @@ export async function runAgent(options) {
129
131
  // report and fed into the compaction ratio - it no longer priced anything,
130
132
  // but it still produced negative token totals and steered a decision about
131
133
  // what to send next.
132
- const usage = response.usage;
134
+ const usage = usageObject(response.usage);
133
135
  const promptTokens = isTokenCount(usage?.prompt_tokens)
134
136
  ? usage.prompt_tokens
135
137
  : undefined;
@@ -161,9 +163,8 @@ export async function runAgent(options) {
161
163
  });
162
164
  if (!canPrice(options.pricing, usage))
163
165
  report.unpricedRequests += 1;
164
- const choice = response.choices?.[0];
165
- const message = choice?.message;
166
- const calls = message?.tool_calls ?? [];
166
+ const message = response.choices[0].message;
167
+ const calls = message.tool_calls ?? [];
167
168
  if (typeof message?.content === "string" && message.content.length > 0) {
168
169
  report.finalText = message.content;
169
170
  options.onEvent?.({ type: "text", text: message.content });
@@ -294,7 +295,7 @@ async function requestWithRateLimitHandling(options, limits, body, report, clock
294
295
  const { data, requestId } = await options.client.postJson("/chat/completions", body, { signal: options.signal });
295
296
  // Carried on the response so the caller can tie this turn to its ledger
296
297
  // row without the loop needing to know what a session is.
297
- return { ...data, requestId };
298
+ return { ...parseChatCompletion(data, "agent API"), requestId };
298
299
  }
299
300
  catch (error) {
300
301
  // Everything except an explicit 429 propagates. A timeout or a 5xx may
@@ -345,9 +346,10 @@ export function backoffMs(retryAfterSeconds, attempt, random = Math.random) {
345
346
  function isTokenCount(value) {
346
347
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
347
348
  }
348
- /** Integer micro-USD, non-negative. Anything else is not a price. */
349
- function isPrice(value) {
350
- return typeof value === "string" && /^\d+$/.test(value);
349
+ function usageObject(value) {
350
+ return value !== null && typeof value === "object"
351
+ ? value
352
+ : undefined;
351
353
  }
352
354
  /**
353
355
  * W-1 · Can this model be priced at all, before anything is sent?
@@ -362,8 +364,8 @@ function isPrice(value) {
362
364
  * direction - a free model would have been refused.
363
365
  */
364
366
  export function hasValidPrices(model) {
365
- return (isPrice(model?.inputPricePer1mMicroUsd) &&
366
- isPrice(model?.outputPricePer1mMicroUsd));
367
+ return (isMicroUsd(model?.inputPricePer1mMicroUsd) &&
368
+ isMicroUsd(model?.outputPricePer1mMicroUsd));
367
369
  }
368
370
  function canPrice(model, usage) {
369
371
  if (usage === undefined)
@@ -379,8 +381,10 @@ function priceTurn(model, usage) {
379
381
  // charged for and reported as unpriced - or the reverse.
380
382
  if (!canPrice(model, usage))
381
383
  return 0n;
382
- const prompt = usage?.prompt_tokens ?? 0;
383
- const completion = usage?.completion_tokens ?? 0;
384
+ const prompt = usage?.prompt_tokens;
385
+ const completion = usage?.completion_tokens;
386
+ if (!isTokenCount(prompt) || !isTokenCount(completion))
387
+ return 0n;
384
388
  const ceilDiv = (tokens, price) => (BigInt(tokens) * BigInt(price) + 999999n) / 1000000n;
385
389
  return (ceilDiv(prompt, model?.inputPricePer1mMicroUsd ?? "0") +
386
390
  ceilDiv(completion, model?.outputPricePer1mMicroUsd ?? "0"));
@@ -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;AA8C5C;;;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;AAoBD,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,CAiC5B"}
@@ -1,4 +1,48 @@
1
1
  import { ApiError } from "./errors.js";
2
+ import { z } from "zod";
3
+ import { isMicroUsd } from "../output/money.js";
4
+ import { parseApiData } from "./validate.js";
5
+ const nonnegativeMoney = z.string().refine(isMicroUsd);
6
+ const count = z.number().safe().int().nonnegative();
7
+ const walletSchema = z
8
+ .object({
9
+ balance_micro_usd: nonnegativeMoney,
10
+ bonus_micro_usd: nonnegativeMoney,
11
+ frozen_micro_usd: nonnegativeMoney,
12
+ available_micro_usd: nonnegativeMoney,
13
+ currency_anchor: z.string(),
14
+ })
15
+ .passthrough();
16
+ const usageSummarySchema = z
17
+ .object({
18
+ days: count,
19
+ from: z.string(),
20
+ to: z.string(),
21
+ count,
22
+ input_tokens: count,
23
+ output_tokens: count,
24
+ total_tokens: count,
25
+ charge_micro_usd: nonnegativeMoney,
26
+ })
27
+ .passthrough();
28
+ const subscriptionSchema = z
29
+ .object({
30
+ status: z.string(),
31
+ plan_slug: z.string().optional(),
32
+ plan_name: z.string().optional(),
33
+ current_period_end: z.string().optional(),
34
+ playground: z
35
+ .object({
36
+ allowance_micro_usd: nonnegativeMoney,
37
+ used_micro_usd: nonnegativeMoney,
38
+ remaining_micro_usd: nonnegativeMoney,
39
+ usage_percent: z.number().finite(),
40
+ state: z.string(),
41
+ })
42
+ .passthrough()
43
+ .optional(),
44
+ })
45
+ .passthrough();
2
46
  /**
3
47
  * The account plane answers 401 with its own error shape when the key lacks
4
48
  * `account:read`. That is a scope problem, not a bad key, and saying so saves
@@ -22,21 +66,38 @@ export async function fetchWallet(client) {
22
66
  const { data } = await client
23
67
  .getJson("/keyinfo/wallet")
24
68
  .catch(explainScope);
25
- return data;
69
+ return parseApiData(walletSchema, data, "wallet API");
26
70
  }
27
71
  export async function fetchUsage(client, days) {
28
72
  const query = days === undefined ? "" : `?days=${days}`;
29
73
  const { data } = await client
30
74
  .getJson(`/keyinfo/usage-summary${query}`)
31
75
  .catch(explainScope);
32
- return data;
76
+ return parseApiData(usageSummarySchema, data, "usage API");
33
77
  }
34
78
  export async function fetchSubscription(client) {
35
79
  const { data } = await client
36
80
  .getJson("/keyinfo/subscription")
37
81
  .catch(explainScope);
38
- return data;
82
+ return parseApiData(subscriptionSchema, data, "subscription API");
39
83
  }
84
+ const usageEventSchema = z
85
+ .object({
86
+ request_id: z.string().min(1),
87
+ model_id: z.string().min(1),
88
+ input_tokens: count,
89
+ output_tokens: count,
90
+ platform_charge_micro_usd: nonnegativeMoney,
91
+ status: z.string().min(1),
92
+ created_at: z.string().min(1),
93
+ })
94
+ .passthrough();
95
+ const usageEventsPageSchema = z
96
+ .object({
97
+ data: z.array(usageEventSchema),
98
+ next_cursor: z.string().nullable().optional(),
99
+ })
100
+ .passthrough();
40
101
  /** Hard stop on paging, so a busy key cannot turn one command into hundreds. */
41
102
  export const MAX_USAGE_EVENT_PAGES = 20;
42
103
  /**
@@ -63,15 +124,16 @@ export async function fetchUsageEvents(client, options = {}) {
63
124
  signal: options.signal,
64
125
  })
65
126
  .catch(explainScope);
127
+ const page = parseApiData(usageEventsPageSchema, data, "usage events API");
66
128
  pages += 1;
67
- for (const event of data.data ?? []) {
129
+ for (const event of page.data) {
68
130
  events.push(event);
69
131
  remaining?.delete(event.request_id);
70
132
  }
71
133
  if (remaining !== undefined && remaining.size === 0) {
72
134
  return { events, pages, exhausted: true };
73
135
  }
74
- cursor = data.next_cursor ?? null;
136
+ cursor = page.next_cursor ?? null;
75
137
  if (cursor === null)
76
138
  return { events, pages, exhausted: true };
77
139
  }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ declare const toolCallSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ type: z.ZodLiteral<"function">;
5
+ function: z.ZodObject<{
6
+ name: z.ZodString;
7
+ arguments: z.ZodString;
8
+ }, z.core.$strip>;
9
+ }, z.core.$loose>;
10
+ declare const chatCompletionSchema: z.ZodObject<{
11
+ choices: z.ZodArray<z.ZodObject<{
12
+ message: z.ZodObject<{
13
+ content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
14
+ tool_calls: z.ZodOptional<z.ZodArray<z.ZodObject<{
15
+ id: z.ZodString;
16
+ type: z.ZodLiteral<"function">;
17
+ function: z.ZodObject<{
18
+ name: z.ZodString;
19
+ arguments: z.ZodString;
20
+ }, z.core.$strip>;
21
+ }, z.core.$loose>>>;
22
+ }, z.core.$loose>;
23
+ finish_reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ }, z.core.$loose>>;
25
+ usage: z.ZodOptional<z.ZodUnknown>;
26
+ }, z.core.$loose>;
27
+ export type ToolCall = z.infer<typeof toolCallSchema>;
28
+ export type ChatCompletion = z.infer<typeof chatCompletionSchema>;
29
+ export declare function parseChatCompletion(value: unknown, label?: string): ChatCompletion;
30
+ export declare function parseChatTextCompletion(value: unknown, label?: string): ChatCompletion;
31
+ export {};
32
+ //# sourceMappingURL=chat-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-response.d.ts","sourceRoot":"","sources":["../../src/api/chat-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,cAAc;;;;;;;iBASJ,CAAC;AAEjB,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;iBAqBV,CAAC;AAMjB,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,KAAK,SAAa,GACjB,cAAc,CAEhB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,KAAK,SAAa,GACjB,cAAc,CAEhB"}
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { parseApiData } from "./validate.js";
3
+ const toolCallSchema = z
4
+ .object({
5
+ id: z.string().min(1),
6
+ type: z.literal("function"),
7
+ function: z.object({
8
+ name: z.string().min(1),
9
+ arguments: z.string(),
10
+ }),
11
+ })
12
+ .passthrough();
13
+ const chatCompletionSchema = z
14
+ .object({
15
+ choices: z
16
+ .array(z
17
+ .object({
18
+ message: z
19
+ .object({
20
+ content: z.string().nullable().optional(),
21
+ tool_calls: z.array(toolCallSchema).optional(),
22
+ })
23
+ .passthrough(),
24
+ finish_reason: z.string().nullable().optional(),
25
+ })
26
+ .passthrough())
27
+ .min(1),
28
+ // Usage is validated independently. A usable, already-billed answer must
29
+ // survive malformed accounting and be reported with unknown usage.
30
+ usage: z.unknown().optional(),
31
+ })
32
+ .passthrough();
33
+ const chatTextCompletionSchema = chatCompletionSchema.refine((response) => typeof response.choices[0]?.message.content === "string", { path: ["choices", 0, "message", "content"] });
34
+ export function parseChatCompletion(value, label = "chat API") {
35
+ return parseApiData(chatCompletionSchema, value, label);
36
+ }
37
+ export function parseChatTextCompletion(value, label = "chat API") {
38
+ return parseApiData(chatTextCompletionSchema, value, label);
39
+ }
@@ -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;AA+B5C,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,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().refine(isUsableModelId) }).passthrough()),
6
+ })
7
+ .passthrough();
8
+ const catalogModelSchema = z
9
+ .object({
10
+ modelId: z.string().refine(isUsableModelId),
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) })
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,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,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"}
@@ -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`);
@@ -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;AAEnE,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;AAiTD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CAYpB"}
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { fetchCatalog } from "../api/models.js";
3
+ import { parseChatTextCompletion } from "../api/chat-response.js";
3
4
  import { readChatStream } from "../api/sse-parser.js";
4
5
  import { ApiError } from "../api/errors.js";
5
6
  import { safeForTerminal, terminalSafeWriter, } from "../output/terminal-safe.js";
@@ -9,7 +10,7 @@ import { upsertIndexEntry } from "../sessions/index-file.js";
9
10
  import { decideRecording } from "../sessions/policy.js";
10
11
  import { SessionWriter } from "../sessions/store.js";
11
12
  import { ExitCode } from "../output/exit-codes.js";
12
- import { formatUsd } from "../output/money.js";
13
+ import { formatUsd, isMicroUsd } from "../output/money.js";
13
14
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
14
15
  const USAGE = `Usage:
15
16
  mafdet chat [prompt] [--model <id>] [--system <file>] [--input <file>]
@@ -275,12 +276,17 @@ async function streamChat(runtime, body, args, io) {
275
276
  return ExitCode.Ok;
276
277
  }
277
278
  async function completeChat(runtime, body, args, io) {
278
- const { data, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
279
- const answer = data.choices?.[0]?.message?.content ?? "";
279
+ const { data: rawData, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, {
280
+ signal: io.signal,
281
+ });
282
+ const data = parseChatTextCompletion(rawData);
283
+ const rawAnswer = data.choices[0].message.content;
284
+ const answer = typeof rawAnswer === "string" ? rawAnswer : "";
285
+ const usage = validUsage(data.usage);
280
286
  io.stdout(`${safeForTerminal(answer, isTty(io))}\n`);
281
287
  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);
288
+ summarise(args, usage, io, model);
289
+ saveChat(args, io, answer, usage, requestId, model);
284
290
  return ExitCode.Ok;
285
291
  }
286
292
  /**
@@ -300,9 +306,32 @@ function summarise(args, usage, io, model, options = {}) {
300
306
  io.stderr(`${prefix}${usage.prompt_tokens} in / ${usage.completion_tokens} out tokens${cost}\n`);
301
307
  }
302
308
  export function estimateChargeMicroUsd(model, usage) {
303
- if (!model?.inputPricePer1mMicroUsd || !model.outputPricePer1mMicroUsd)
309
+ if (!isMicroUsd(model?.inputPricePer1mMicroUsd) ||
310
+ !isMicroUsd(model.outputPricePer1mMicroUsd))
304
311
  return undefined;
305
312
  const ceilDiv = (tokens, pricePer1m) => (BigInt(tokens) * BigInt(pricePer1m) + 999999n) / 1000000n;
306
313
  return (ceilDiv(usage.prompt_tokens, model.inputPricePer1mMicroUsd) +
307
314
  ceilDiv(usage.completion_tokens, model.outputPricePer1mMicroUsd));
308
315
  }
316
+ function validUsage(value) {
317
+ if (value === null || typeof value !== "object")
318
+ return undefined;
319
+ const usage = value;
320
+ const count = (field) => {
321
+ const candidate = usage[field];
322
+ return typeof candidate === "number" &&
323
+ Number.isSafeInteger(candidate) &&
324
+ candidate >= 0
325
+ ? candidate
326
+ : undefined;
327
+ };
328
+ const prompt = count("prompt_tokens");
329
+ const completion = count("completion_tokens");
330
+ if (prompt === undefined || completion === undefined)
331
+ return undefined;
332
+ return {
333
+ prompt_tokens: prompt,
334
+ completion_tokens: completion,
335
+ total_tokens: count("total_tokens") ?? prompt + completion,
336
+ };
337
+ }
@@ -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
  ]));
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAetB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAoRD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgBtB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAoRD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
@@ -30,7 +30,8 @@ Conversations are stored on this computer only. Browsing them never
30
30
  calls a model and is never charged.
31
31
 
32
32
  --refresh asks the billing ledger what this key's requests actually
33
- cost. It sends request IDs and nothing else - no messages, no titles.
33
+ cost. It queries this key's usage-event window; request IDs stay local
34
+ for matching. No messages, titles, workspace or session ID are sent.
34
35
  `;
35
36
  export async function sessionsCommand(argv, io) {
36
37
  // F-013 · Nothing here may reach the top level as a raw Error. `--json
@@ -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":"reconcile.d.ts","sourceRoot":"","sources":["../../src/sessions/reconcile.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,gBAAgB,EAAE,MAAM,GACvB,aAAa,CAwCf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;IAC9C,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,gBAAgB,EAAE,EACnC,UAAU,EAAE,MAAM,GACjB,WAAW,CAyBb;AAOD;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAM7E"}
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/sessions/reconcile.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,gBAAgB,EAAE,MAAM,GACvB,aAAa,CAwCf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;IAC9C,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,gBAAgB,EAAE,EACnC,UAAU,EAAE,MAAM,GACjB,WAAW,CAiCb;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAM7E"}
@@ -12,6 +12,7 @@
12
12
  // caught up with yet, and the user is told to wait for something that will
13
13
  // never arrive. With it, we can say which key to run this under instead.
14
14
  import { isComparableKeyRef } from "../security/redact.js";
15
+ import { isMicroUsd } from "../output/money.js";
15
16
  /**
16
17
  * Decide what to ask about, from the transcript alone.
17
18
  *
@@ -69,8 +70,13 @@ export function planReconciliation(events, currentKeyDigest) {
69
70
  export function matchSettlements(plan, ledger, observedAt) {
70
71
  const byId = new Map();
71
72
  for (const row of ledger) {
72
- if (plan.wanted.has(row.request_id))
73
+ // Defense in depth: the API boundary rejects this already. If an invalid
74
+ // row is nevertheless injected, it remains pending instead of becoming a
75
+ // fabricated zero-dollar settlement.
76
+ if (plan.wanted.has(row.request_id) &&
77
+ isMicroUsd(row.platform_charge_micro_usd)) {
73
78
  byId.set(row.request_id, row);
79
+ }
74
80
  }
75
81
  const settlements = [];
76
82
  const missing = [];
@@ -85,17 +91,13 @@ export function matchSettlements(plan, ledger, observedAt) {
85
91
  request_id: id,
86
92
  // Kept as the string the server sent. Parsing it into a number here is
87
93
  // how integer micro-USD becomes a float somewhere downstream.
88
- settled_charge_micro_usd: normaliseMicro(row.platform_charge_micro_usd),
94
+ settled_charge_micro_usd: row.platform_charge_micro_usd,
89
95
  server_status: typeof row.status === "string" ? row.status : "unknown",
90
96
  observed_at: observedAt,
91
97
  });
92
98
  }
93
99
  return { settlements, missing };
94
100
  }
95
- /** Anything that is not an integer count of micro-USD is not a charge. */
96
- function normaliseMicro(value) {
97
- return typeof value === "string" && /^-?\d+$/.test(value) ? value : "0";
98
- }
99
101
  /**
100
102
  * How many days back the ledger has to be asked about.
101
103
  *
@@ -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.10",
3
+ "version": "0.4.12",
4
4
  "description": "Mafdet AI command line — model calls, account queries, and a permissioned local agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",