@mafdet/cli 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,69 @@ 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.10 - 2026-08-12
10
+
11
+ A sixth audit round, and the finding that matters most is about how the last
12
+ one was tested.
13
+
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.
20
+
21
+ The test that was supposed to cover this called the sanitiser directly, so it
22
+ passed while every caller bypassed it. That is the same mistake as the one in
23
+ 0.4.7, one release later. The new tests go in at `embedCommand`,
24
+ `speechCommand` and `agentCommand` - the commands a user actually runs - and
25
+ each was checked to fail when its fix is removed.
26
+
27
+ **A cost ceiling refuses an unusable price before sending anything.** The
28
+ pre-flight check asked only whether the price fields were present, so `abc`,
29
+ `-1`, `1.5` and `" 2"` passed it and the run spent one request before the
30
+ post-response check refused the result. Both checks now share one definition,
31
+ which also fixes the opposite error: a price of `"0"` is a price, and a truthy
32
+ test was rejecting free models.
33
+
34
+ **An invalid token count no longer reaches the totals.** It had stopped
35
+ affecting cost, but a negative or fractional figure was still added to the
36
+ token summary and fed into the context estimate - producing negative totals
37
+ and steering a decision about what to send next.
38
+
39
+ ## 0.4.9 - 2026-08-12
40
+
41
+ A fifth audit round: two billing and terminal-safety fixes, and two smaller
42
+ corrections.
43
+
44
+ **Incomplete usage is no longer priced as though the missing half were zero.**
45
+ A reply carrying `{}`, or only `prompt_tokens`, or only `completion_tokens`,
46
+ was charged with the absent side counted as nothing - so a run under
47
+ `--max-cost-usd` finished successfully having applied the ceiling to a partly
48
+ invented figure. A fractional count did worse: it crashed with an uncaught
49
+ error out of the pricing code. Both token counts and both prices must now be
50
+ present and valid; anything else is reported as unpriced, stops a run that
51
+ asked for a ceiling, and keeps the answer that was generated.
52
+
53
+ **Errors and diagnostics can no longer carry terminal control sequences.**
54
+ ⚠️ Not true as written: `doctor` and the shared reporter were fixed, but
55
+ `embed`, `speech`, the agent's summary and the request id still bypassed it.
56
+ Fixed in 0.4.10.
57
+ `mafdet doctor` interpolated a model's `apiSurface` straight into its table,
58
+ and a surface-mismatch error did the same into stderr. Rather than cleaning
59
+ those two strings, the cleaning now happens at the two boundaries every
60
+ command already funnels through - the shared error reporter and the diagnostic
61
+ table - so the checks and messages added later are covered too. Redirected
62
+ output and `--json` keep the bytes exactly.
63
+
64
+ **The context estimate measures UTF-8 bytes**, not UTF-16 code units, and
65
+ counts the whole request body. A Chinese conversation measured about 10%
66
+ smaller than it is on the wire, in the one estimate that must not under-count.
67
+
68
+ **Unused code is now a compile error** (`noUnusedLocals`,
69
+ `noUnusedParameters`). A dead import survived four review rounds because
70
+ nothing was looking for one; turning the check on found eight more.
71
+
9
72
  ## 0.4.8 - 2026-08-12
10
73
 
11
74
  A fourth audit round: three findings, one of which was a regression I
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.8.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.10.** 中文版见 [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.8.tgz
27
- npm install -g ./mafdet-cli-0.4.8.tgz
26
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.10.tgz
27
+ npm install -g ./mafdet-cli-0.4.10.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.8。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.10。** 英文版见 [README.md](./README.md)。
6
6
 
7
7
  ## 环境要求
8
8
 
@@ -94,7 +94,7 @@ export declare function compactForBudget(history: ChatMessage[], keepRecent?: nu
94
94
  * is not a constant across English, Chinese, code and JSON.
95
95
  *
96
96
  * What is measured here instead is the ratio between the provider's own token
97
- * count and the exact bytes we sent to earn it. Applied to the bytes we are
97
+ * count and the UTF-8 size of what we sent to earn it. Applied to the bytes we are
98
98
  * about to send - schemas included - it needs no assumption about language or
99
99
  * encoding, because both sides of the ratio came from the same request.
100
100
  *
@@ -105,10 +105,24 @@ export declare function compactForBudget(history: ChatMessage[], keepRecent?: nu
105
105
  * counted rather than assumed away.
106
106
  */
107
107
  export declare function compactUntilItFits(history: ChatMessage[], contextLength: number | undefined, lastPromptTokens: number,
108
- /** Bytes of the request that produced `lastPromptTokens`. */
109
- lastRequestBytes: number,
110
- /** Bytes of the request as it stands now, recomputed after each removal. */
108
+ /** Size of the request that produced `lastPromptTokens`, in UTF-8 bytes. */
109
+ lastRequestSize: number,
110
+ /** Size of the request as it stands now, recomputed after each removal. */
111
111
  sizeNow: () => number, keepRecent?: number): CompactionResult;
112
- /** The bytes a request occupies on the wire, schemas and all. */
113
- export declare function requestBytes(messages: readonly ChatMessage[], tools: unknown): number;
112
+ /**
113
+ * V-3 · The size of a request, as UTF-8 bytes.
114
+ *
115
+ * Named `requestSize` rather than `requestBytes` for a while it was neither:
116
+ * `String.length` counts UTF-16 code units, so a Chinese conversation measured
117
+ * about 10% smaller than it is on the wire - and the estimate that uses this
118
+ * is precisely the one that must not under-count. `Buffer.byteLength` counts
119
+ * what is actually sent.
120
+ *
121
+ * ⚠️ It feeds a heuristic, not a tokenizer. The ratio it participates in is
122
+ * measured from a real request, which is the best available answer without a
123
+ * server-side token count - but it cannot prove a request fits the window.
124
+ */
125
+ export declare function requestSize(messages: readonly ChatMessage[], tools: unknown,
126
+ /** The rest of the body, so the measurement covers what is really sent. */
127
+ extra?: Record<string, unknown>): number;
114
128
  //# sourceMappingURL=context-budget.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0BpC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM;AACxB,6DAA6D;AAC7D,gBAAgB,EAAE,MAAM;AACxB,4EAA4E;AAC5E,OAAO,EAAE,MAAM,MAAM,EACrB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CA8BlB;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,KAAK,EAAE,OAAO,GACb,MAAM,CAQR"}
1
+ {"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0BpC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM;AACxB,4EAA4E;AAC5E,eAAe,EAAE,MAAM;AACvB,2EAA2E;AAC3E,OAAO,EAAE,MAAM,MAAM,EACrB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CA8BlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,KAAK,EAAE,OAAO;AACd,2EAA2E;AAC3E,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,MAAM,CAWR"}
@@ -192,7 +192,7 @@ export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
192
192
  * is not a constant across English, Chinese, code and JSON.
193
193
  *
194
194
  * What is measured here instead is the ratio between the provider's own token
195
- * count and the exact bytes we sent to earn it. Applied to the bytes we are
195
+ * count and the UTF-8 size of what we sent to earn it. Applied to the bytes we are
196
196
  * about to send - schemas included - it needs no assumption about language or
197
197
  * encoding, because both sides of the ratio came from the same request.
198
198
  *
@@ -203,12 +203,12 @@ export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
203
203
  * counted rather than assumed away.
204
204
  */
205
205
  export function compactUntilItFits(history, contextLength, lastPromptTokens,
206
- /** Bytes of the request that produced `lastPromptTokens`. */
207
- lastRequestBytes,
208
- /** Bytes of the request as it stands now, recomputed after each removal. */
206
+ /** Size of the request that produced `lastPromptTokens`, in UTF-8 bytes. */
207
+ lastRequestSize,
208
+ /** Size of the request as it stands now, recomputed after each removal. */
209
209
  sizeNow, keepRecent = KEEP_RECENT_EXCHANGES) {
210
210
  const startedWith = history.length;
211
- const tokensPerByte = lastRequestBytes > 0 ? lastPromptTokens / lastRequestBytes : 0;
211
+ const tokensPerByte = lastRequestSize > 0 ? lastPromptTokens / lastRequestSize : 0;
212
212
  let exchanges = 0;
213
213
  let turns = 0;
214
214
  for (;;) {
@@ -235,10 +235,24 @@ sizeNow, keepRecent = KEEP_RECENT_EXCHANGES) {
235
235
  turns += step.turns;
236
236
  }
237
237
  }
238
- /** The bytes a request occupies on the wire, schemas and all. */
239
- export function requestBytes(messages, tools) {
238
+ /**
239
+ * V-3 · The size of a request, as UTF-8 bytes.
240
+ *
241
+ * Named `requestSize` rather than `requestBytes` for a while it was neither:
242
+ * `String.length` counts UTF-16 code units, so a Chinese conversation measured
243
+ * about 10% smaller than it is on the wire - and the estimate that uses this
244
+ * is precisely the one that must not under-count. `Buffer.byteLength` counts
245
+ * what is actually sent.
246
+ *
247
+ * ⚠️ It feeds a heuristic, not a tokenizer. The ratio it participates in is
248
+ * measured from a real request, which is the best available answer without a
249
+ * server-side token count - but it cannot prove a request fits the window.
250
+ */
251
+ export function requestSize(messages, tools,
252
+ /** The rest of the body, so the measurement covers what is really sent. */
253
+ extra = {}) {
240
254
  try {
241
- return JSON.stringify({ messages, tools }).length;
255
+ return Buffer.byteLength(JSON.stringify({ ...extra, messages, tools }), "utf8");
242
256
  }
243
257
  catch {
244
258
  // Circular or unserialisable: fall back to something monotone in the
@@ -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":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ApiError } from "../api/errors.js";
2
- import { compactUntilItFits, requestBytes, shouldCompact, } from "./context-budget.js";
2
+ import { compactUntilItFits, requestSize, shouldCompact, } from "./context-budget.js";
3
3
  import { toolToJsonSchema, } from "./tools/types.js";
4
4
  /** Plan §7.4. */
5
5
  export const DEFAULT_LIMITS = {
@@ -48,17 +48,16 @@ export async function runAgent(options) {
48
48
  };
49
49
  /** What the provider counted for the previous request; 0 until one lands. */
50
50
  let lastPromptTokens = 0;
51
- /** U-1 · Bytes of the request that produced `lastPromptTokens`. */
52
- let lastRequestBytes = 0;
51
+ /** U-1 · UTF-8 size of the request that produced `lastPromptTokens`. */
52
+ let lastRequestSize = 0;
53
53
  // T-3 · Known before anything is sent. Waiting for the first response to
54
54
  // discover it meant a run with an explicit ceiling still made one billable
55
55
  // request that could never be counted against it.
56
56
  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.`);
57
+ !hasValidPrices(options.pricing)) {
58
+ return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue publishes no usable ` +
59
+ `price for ${options.model}, so nothing can be counted towards the ` +
60
+ `ceiling. Re-run without the limit to continue anyway.`);
62
61
  }
63
62
  for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
64
63
  if (options.signal?.aborted)
@@ -76,7 +75,10 @@ export async function runAgent(options) {
76
75
  // R-2 · Keep going until the estimate is inside the budget. Dropping one
77
76
  // exchange and sending anyway left a history that was still over the
78
77
  // window: the request failed upstream and was billed for the attempt.
79
- const compacted = compactUntilItFits(messages, options.pricing?.contextLength, lastPromptTokens, lastRequestBytes, () => requestBytes(messages, toolSchemas));
78
+ const compacted = compactUntilItFits(messages, options.pricing?.contextLength, lastPromptTokens, lastRequestSize, () => requestSize(messages, toolSchemas, {
79
+ model: options.model,
80
+ tool_choice: "auto",
81
+ }));
80
82
  if (compacted.exhausted) {
81
83
  // F-008 · Nothing left that can be dropped without breaking the
82
84
  // conversation. Sending anyway means a request over the window, which
@@ -100,7 +102,10 @@ export async function runAgent(options) {
100
102
  }
101
103
  // U-1 · Measured for the request we are about to make, so the ratio the
102
104
  // next compaction uses is the provider's own count over these exact bytes.
103
- const sentBytes = requestBytes(messages, toolSchemas);
105
+ const sentSize = requestSize(messages, toolSchemas, {
106
+ model: options.model,
107
+ tool_choice: "auto",
108
+ });
104
109
  let response;
105
110
  try {
106
111
  response = await requestWithRateLimitHandling(options, limits, {
@@ -119,13 +124,24 @@ export async function runAgent(options) {
119
124
  }
120
125
  return stop(report, "error", error instanceof Error ? error.message : String(error));
121
126
  }
127
+ // W-3 · Only counts that survive validation reach the totals or the
128
+ // context state. A negative or fractional figure used to be added to the
129
+ // report and fed into the compaction ratio - it no longer priced anything,
130
+ // but it still produced negative token totals and steered a decision about
131
+ // what to send next.
122
132
  const usage = response.usage;
123
- if (usage?.prompt_tokens !== undefined) {
124
- lastPromptTokens = usage.prompt_tokens;
125
- lastRequestBytes = sentBytes;
133
+ const promptTokens = isTokenCount(usage?.prompt_tokens)
134
+ ? usage.prompt_tokens
135
+ : undefined;
136
+ const completionTokens = isTokenCount(usage?.completion_tokens)
137
+ ? usage.completion_tokens
138
+ : undefined;
139
+ if (promptTokens !== undefined) {
140
+ lastPromptTokens = promptTokens;
141
+ lastRequestSize = sentSize;
126
142
  }
127
- report.promptTokens += usage?.prompt_tokens ?? 0;
128
- report.completionTokens += usage?.completion_tokens ?? 0;
143
+ report.promptTokens += promptTokens ?? 0;
144
+ report.completionTokens += completionTokens ?? 0;
129
145
  // Cost comes from returned usage, never from the reservation: CLI-000B
130
146
  // showed the reserve cannot see the tool schema and under-counts badly.
131
147
  const turnCost = priceTurn(options.pricing, usage);
@@ -136,8 +152,10 @@ export async function runAgent(options) {
136
152
  ...(response.requestId === undefined
137
153
  ? {}
138
154
  : { requestId: response.requestId }),
139
- promptTokens: usage?.prompt_tokens ?? 0,
140
- completionTokens: usage?.completion_tokens ?? 0,
155
+ // W-3 · The validated values, so nothing downstream - the recorder, the
156
+ // session store, a JSON consumer - has to re-derive what was usable.
157
+ promptTokens: promptTokens ?? 0,
158
+ completionTokens: completionTokens ?? 0,
141
159
  costMicroUsd: turnCost,
142
160
  priced: canPrice(options.pricing, usage),
143
161
  });
@@ -157,9 +175,9 @@ export async function runAgent(options) {
157
175
  // had never been applied to anything.
158
176
  if (limits.maxCostMicroUsd !== undefined &&
159
177
  !canPrice(options.pricing, usage)) {
160
- return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the response for ${options.model} ` +
161
- `carried no usage, so this request could not be counted towards ` +
162
- `the ceiling.`);
178
+ return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: usage was missing or incomplete ` +
179
+ `in the response for ${options.model}, so this request could not be ` +
180
+ `counted towards the ceiling.`);
163
181
  }
164
182
  if (calls.length === 0) {
165
183
  // Keep the answer in the history so a session's next turn has context.
@@ -311,21 +329,61 @@ export function backoffMs(retryAfterSeconds, attempt, random = Math.random) {
311
329
  // Full jitter: a fleet of CLIs must not retry in lockstep.
312
330
  return Math.round(base * (0.5 + random() * 0.5));
313
331
  }
314
- /** F-012 · Whether `priceTurn` had everything it needed to give an answer. */
332
+ /**
333
+ * V-1 · Whether this turn can be priced at all.
334
+ *
335
+ * "There is a usage object" is not the same as "there is a usage". A reply can
336
+ * carry `{}`, or only `prompt_tokens`, or a fractional or negative count - and
337
+ * each of those used to be priced as though the missing half were zero, so a
338
+ * run under `--max-cost-usd` finished successfully having applied the ceiling
339
+ * to a number that was partly invented. A fractional count did worse: it threw
340
+ * out of `priceTurn` as an uncaught RangeError.
341
+ *
342
+ * Both token counts and both prices have to be there and be usable. Anything
343
+ * short of that is unpriced, which the caller reports rather than rounds down.
344
+ */
345
+ function isTokenCount(value) {
346
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
347
+ }
348
+ /** Integer micro-USD, non-negative. Anything else is not a price. */
349
+ function isPrice(value) {
350
+ return typeof value === "string" && /^\d+$/.test(value);
351
+ }
352
+ /**
353
+ * W-1 · Can this model be priced at all, before anything is sent?
354
+ *
355
+ * Exported and shared with the pre-flight check, because the two used to
356
+ * disagree: the check asked only whether the fields were truthy, so `abc`,
357
+ * `-1`, `1.5` and `" 2"` all passed it and the run spent one request before
358
+ * `canPrice` refused the result. A ceiling that cannot cover the first request
359
+ * is not the ceiling the user asked for.
360
+ *
361
+ * Note `"0"` is a valid price and truthy checks got that wrong in the other
362
+ * direction - a free model would have been refused.
363
+ */
364
+ export function hasValidPrices(model) {
365
+ return (isPrice(model?.inputPricePer1mMicroUsd) &&
366
+ isPrice(model?.outputPricePer1mMicroUsd));
367
+ }
315
368
  function canPrice(model, usage) {
316
- return (usage !== undefined &&
317
- model?.inputPricePer1mMicroUsd !== undefined &&
318
- model?.outputPricePer1mMicroUsd !== undefined);
369
+ if (usage === undefined)
370
+ return false;
371
+ if (!isTokenCount(usage.prompt_tokens))
372
+ return false;
373
+ if (!isTokenCount(usage.completion_tokens))
374
+ return false;
375
+ return hasValidPrices(model);
319
376
  }
320
377
  function priceTurn(model, usage) {
321
- if (!model?.inputPricePer1mMicroUsd ||
322
- !model.outputPricePer1mMicroUsd ||
323
- !usage) {
378
+ // V-1 · One gate, shared with the caller's reporting, so a turn can never be
379
+ // charged for and reported as unpriced - or the reverse.
380
+ if (!canPrice(model, usage))
324
381
  return 0n;
325
- }
326
- const ceilDiv = (tokens, price) => (BigInt(Math.max(0, tokens)) * BigInt(price) + 999999n) / 1000000n;
327
- return (ceilDiv(usage.prompt_tokens ?? 0, model.inputPricePer1mMicroUsd) +
328
- ceilDiv(usage.completion_tokens ?? 0, model.outputPricePer1mMicroUsd));
382
+ const prompt = usage?.prompt_tokens ?? 0;
383
+ const completion = usage?.completion_tokens ?? 0;
384
+ const ceilDiv = (tokens, price) => (BigInt(tokens) * BigInt(price) + 999999n) / 1000000n;
385
+ return (ceilDiv(prompt, model?.inputPricePer1mMicroUsd ?? "0") +
386
+ ceilDiv(completion, model?.outputPricePer1mMicroUsd ?? "0"));
329
387
  }
330
388
  function stop(report, reason, error) {
331
389
  report.stopReason = reason;
@@ -38,7 +38,7 @@ export async function runSession(options) {
38
38
  let pricing = options.pricing;
39
39
  const source = options.readLine
40
40
  ? undefined
41
- : lineSource(options.input ?? process.stdin, options.stderr);
41
+ : lineSource(options.input ?? process.stdin);
42
42
  const readLine = options.readLine ??
43
43
  (() => {
44
44
  options.stderr("\n> ");
@@ -253,7 +253,7 @@ export function compactHistory(history) {
253
253
  * a queue keeps them, and end-of-input only reports undefined once the queue
254
254
  * is actually empty.
255
255
  */
256
- function lineSource(input, write) {
256
+ function lineSource(input) {
257
257
  const rl = createInterface({
258
258
  input,
259
259
  output: process.stderr,
@@ -55,7 +55,7 @@ export async function executeCommand(argv, cwd, timeoutMs = COMMAND_TIMEOUT_MS)
55
55
  clearTimeout(timer);
56
56
  resolve({ code, signal, output: stripAnsi(output), timedOut });
57
57
  };
58
- child.on("error", (error) => finish(null, null));
58
+ child.on("error", () => finish(null, null));
59
59
  child.on("close", (code, signal) => finish(code, signal));
60
60
  });
61
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AACA,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":"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 +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,CAuInB;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>]
@@ -383,8 +383,9 @@ function finish(report, json, io) {
383
383
  `~${formatUsdWithUnpriced(report.costMicroUsd, report.unpricedRequests)}`,
384
384
  ],
385
385
  ]));
386
+ // W-2 · `report.error` is the upstream's message, verbatim.
386
387
  if (report.error)
387
- io.stderr(`error: ${report.error}\n`);
388
+ writeHumanError(io, `error: ${report.error}\n`);
388
389
  }
389
390
  switch (report.stopReason) {
390
391
  case "completed":
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAInD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAY5D,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,QAAQ,CAAC,CAyFnB"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAKnD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAY5D,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,QAAQ,CAAC,CAyFnB"}
@@ -7,6 +7,7 @@ import { ConfigError } from "../config/config.js";
7
7
  import { ExitCode } from "../output/exit-codes.js";
8
8
  import { parseFormatFlags, renderJson, renderTable } from "../output/format.js";
9
9
  import { formatUsd } from "../output/money.js";
10
+ import { safeForTerminal } from "../output/terminal-safe.js";
10
11
  import { keyFingerprint, redact } from "../security/redact.js";
11
12
  import { buildRuntime } from "./runtime.js";
12
13
  const MINIMUM_NODE_MAJOR = 20;
@@ -141,14 +142,20 @@ function describe(error) {
141
142
  function report(checks, format, io) {
142
143
  const failed = checks.filter((check) => check.status === "fail").length;
143
144
  if (format === "json") {
145
+ // Exact values, as everywhere else that a machine reads.
144
146
  io.stdout(renderJson({ ok: failed === 0, checks }));
145
147
  }
146
148
  else {
147
149
  const mark = { ok: "ok", warn: "warn", fail: "FAIL" };
150
+ // V-2 · One place every check's detail is drawn, and several of them are
151
+ // built from remote text - a model's apiSurface, an endpoint, an upstream
152
+ // error. Cleaning here covers the checks that exist and the ones added
153
+ // later; cleaning each check's string did not.
154
+ const tty = io.isTty ?? process.stdout.isTTY === true;
148
155
  io.stdout(renderTable(checks, [
149
156
  { header: "", value: (c) => mark[c.status] },
150
- { header: "CHECK", value: (c) => c.name },
151
- { header: "DETAIL", value: (c) => c.detail },
157
+ { header: "CHECK", value: (c) => safeForTerminal(c.name, tty) },
158
+ { header: "DETAIL", value: (c) => safeForTerminal(c.detail, tty) },
152
159
  ]));
153
160
  }
154
161
  // Warnings are survivable — a missing scope or an unverified model does not
@@ -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);
@@ -16,6 +16,12 @@ export interface CommandIo {
16
16
  * Defaults to `process.stdout.isTTY`; tests set it explicitly.
17
17
  */
18
18
  isTty?: boolean;
19
+ /**
20
+ * V-2 · Whether stderr is a terminal. Separate from `isTty` because
21
+ * `mafdet ... 2> log` redirects one and not the other, and the rule is about
22
+ * the destination rather than the command.
23
+ */
24
+ isErrTty?: boolean;
19
25
  }
20
26
  export interface Runtime {
21
27
  config: LoadedConfig;
@@ -31,5 +37,30 @@ export declare function requireApiKey(config: LoadedConfig): void;
31
37
  * Turn any failure into an exit code and a single stderr line. Nothing here
32
38
  * prints a raw upstream message: ApiError redacted it at construction.
33
39
  */
40
+ /**
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.
57
+ *
58
+ * Every command funnels through here, so this is where untrusted text in an
59
+ * error message stops being able to drive the terminal. Patching the
60
+ * individual messages was tried and missed two of them; a message is
61
+ * assembled from remote data far more often than anyone remembers.
62
+ *
63
+ * Redirected stderr keeps the bytes, the same rule as everywhere else.
64
+ */
34
65
  export declare function reportError(error: unknown, io: CommandIo): ExitCode;
35
66
  //# sourceMappingURL=runtime.d.ts.map
@@ -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;AAEnD,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;CACjB;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,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAkBnE"}
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"}
@@ -3,6 +3,7 @@ import { loadConfig } from "../config/load.js";
3
3
  import { HttpClient } from "../api/http.js";
4
4
  import { ApiError } from "../api/errors.js";
5
5
  import { ExitCode } from "../output/exit-codes.js";
6
+ import { safeForTerminal } from "../output/terminal-safe.js";
6
7
  export function userAgent(version) {
7
8
  // Identifiable, and carrying nothing about the user (plan §9.2).
8
9
  return `mafdet-cli/${version}`;
@@ -37,20 +38,50 @@ export function requireApiKey(config) {
37
38
  * Turn any failure into an exit code and a single stderr line. Nothing here
38
39
  * prints a raw upstream message: ApiError redacted it at construction.
39
40
  */
41
+ /**
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.
60
+ *
61
+ * Every command funnels through here, so this is where untrusted text in an
62
+ * error message stops being able to drive the terminal. Patching the
63
+ * individual messages was tried and missed two of them; a message is
64
+ * assembled from remote data far more often than anyone remembers.
65
+ *
66
+ * Redirected stderr keeps the bytes, the same rule as everywhere else.
67
+ */
40
68
  export function reportError(error, io) {
41
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.
42
73
  const id = error.requestId ? ` (request ${error.requestId})` : "";
43
- io.stderr(`mafdet: ${error.message}${id}\n`);
74
+ writeHumanError(io, `mafdet: ${error.message}${id}\n`);
44
75
  if (error.kind === "rate_limited" &&
45
76
  error.retryAfterSeconds !== undefined) {
46
- io.stderr(`mafdet: retry after ${error.retryAfterSeconds}s\n`);
77
+ writeHumanError(io, `mafdet: retry after ${error.retryAfterSeconds}s\n`);
47
78
  }
48
79
  return error.exitCode;
49
80
  }
50
81
  if (error instanceof ConfigError) {
51
- io.stderr(`mafdet: ${error.message}\n`);
82
+ writeHumanError(io, `mafdet: ${error.message}\n`);
52
83
  return ExitCode.Usage;
53
84
  }
54
- io.stderr(`mafdet: ${error.message}\n`);
85
+ writeHumanError(io, `mafdet: ${error.message}\n`);
55
86
  return ExitCode.General;
56
87
  }
@@ -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;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"}
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 +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
@@ -1 +1 @@
1
- {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAEL,oBAAoB,EAGpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,aAAa,EAAe,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;CAC/D,CAAC;AAEN,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAqFnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAC9C,MAAM,CAyBR"}
1
+ {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sessions/open.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAEL,oBAAoB,EAGpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,aAAa,EAAe,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;CAC/D,CAAC;AAEN,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAqFnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAC9C,MAAM,CAyBR"}
@@ -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) {
@@ -53,7 +53,7 @@ export async function promptLine(io, prompt) {
53
53
  }
54
54
  export async function runAgentSurface(options) {
55
55
  const { io, capabilities } = options;
56
- const { dim, bold } = palette(capabilities);
56
+ const { dim } = palette(capabilities);
57
57
  const cards = new ToolCardLog();
58
58
  const run = options.run ?? runSession;
59
59
  const onEvent = (event) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mafdet/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Mafdet AI command line — model calls, account queries, and a permissioned local agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",