@mafdet/cli 0.4.5 → 0.4.6

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,44 @@ 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.6 - 2026-08-12
10
+
11
+ A third audit round. Two of its findings were things the previous round's
12
+ notes recorded as fixed, and the pattern is worth naming: a partial fix logged
13
+ as complete is not looked at again.
14
+
15
+ **An interactive `mafdet agent` now records your questions.** It saved the
16
+ model's replies, the tool runs and the usage, and none of the prompts - so
17
+ resuming such a session could not continue a conversation that was only half
18
+ there. The TUI wrapped its own reader to record them and the command surface
19
+ did not; recording now happens in the session loop, where neither can forget.
20
+
21
+ **`--max-cost-usd` refuses before spending, not after.** When the catalogue
22
+ publishes no price for a model, the run used to make one billable request and
23
+ discover the problem from the response. Whether a price exists is known before
24
+ anything is sent.
25
+
26
+ **A run nobody could price no longer reports `$0.00`.** The command summary,
27
+ the TUI summary, `/cost` and the JSON report all showed zero for a run whose
28
+ model had no published price; they now say how many requests the figure does
29
+ not cover, as `sessions usage` already did.
30
+
31
+ **Context compaction accounts for what cannot shrink.** The estimate assumed
32
+ tokens fall in proportion to messages, but the system prompt and tool schemas
33
+ are re-sent every turn - so it could decide a history had come inside the
34
+ window when only the removable part had.
35
+
36
+ **Model catalogue text is sanitised where it enters.** Terminal escape
37
+ sequences were being stripped sink by sink, and the model picker was still
38
+ missing after two rounds; cleaning the catalogue on arrival leaves nothing
39
+ downstream to forget. Fixed with it: stripping a sequence no longer eats the
40
+ text that follows it, which an 8-bit OSC terminated by BEL used to do.
41
+
42
+ Also: the stream's total deadline actually bounds the wait instead of allowing
43
+ one more idle period; compaction reports how many turns it dropped rather than
44
+ always one; the session picker and the per-model and per-key JSON carry the
45
+ unpriced count.
46
+
9
47
  ## 0.4.5 - 2026-08-12
10
48
 
11
49
  A second audit round, and the security fix in it is the important one.
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.5.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.6.** 中文版见 [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.5.tgz
27
- npm install -g ./mafdet-cli-0.4.5.tgz
26
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.6.tgz
27
+ npm install -g ./mafdet-cli-0.4.6.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.5。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.6。** 英文版见 [README.md](./README.md)。
6
6
 
7
7
  ## 环境要求
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"command-policy.d.ts","sourceRoot":"","sources":["../../src/agent/command-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,cAAc,CAwChB;AAmBD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwB9C"}
1
+ {"version":3,"file":"command-policy.d.ts","sourceRoot":"","sources":["../../src/agent/command-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,cAAc,CAwChB;AAmBD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B9C"}
@@ -119,24 +119,28 @@ function matchesAllowlist(argv, allowlist) {
119
119
  /** Terminal control sequences, stripped before any output is shown or sent. */
120
120
  export function stripAnsi(text) {
121
121
  return (text
122
- // CSI: ESC [ ... final byte - colours, cursor moves, screen clears.
122
+ // Whole sequences first, in both their 7-bit and 8-bit forms, and only
123
+ // then the leftover control characters.
124
+ //
125
+ // The order is load-bearing and was wrong once: removing C0 controls
126
+ // before parsing the 8-bit OSC took away the BEL that terminates it, so
127
+ // the pattern ran to the end of the string and swallowed the ordinary
128
+ // text after the sequence. Failing safe, but destroying output.
129
+ //
130
+ // CSI: colours, cursor moves, screen clears.
123
131
  .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
124
- // OSC: ESC ] ... BEL (or ST). The payload has to go too - stripping only
125
- // the introducer would leave "0;retitled" sitting in the output.
126
- .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, "")
127
- // Other single-character escapes, including OSC introducers.
132
+ .replace(/\u009b[0-?]*[ -/]*[@-~]/g, "")
133
+ // OSC: window titles and the like. The payload goes with the
134
+ // introducer - stripping only the introducer would leave "0;retitled"
135
+ // sitting in the output. Terminated by BEL or by ST in either form.
136
+ .replace(/\u001b\][^\u0007\u001b\u009c]*(?:\u0007|\u001b\\|\u009c)?/g, "")
137
+ .replace(/\u009d[^\u0007\u001b\u009c]*(?:\u0007|\u001b\\|\u009c)?/g, "")
138
+ // Other single-character escapes.
128
139
  .replace(/\u001b[@-Z\\-_]/g, "")
129
140
  // Remaining C0 controls. Tab, newline and carriage return survive; the
130
141
  // rest can retitle a window, move the cursor or hide what follows, and a
131
142
  // model's output is not a trusted source of terminal instructions.
132
143
  .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
133
- // R-4 · The 8-bit C1 forms of the same sequences. A terminal in 8-bit
134
- // mode reads U+009B as CSI and U+009D as OSC - identical instructions to
135
- // the ESC forms above, with no ESC to match on. Taken as whole
136
- // sequences, so the payload goes with the introducer rather than being
137
- // left behind as visible text.
138
- .replace(/\u009b[0-?]*[ -/]*[@-~]/g, "")
139
- .replace(/\u009d[^\u0007\u009c]*(?:\u0007|\u009c)?/g, "")
140
- // Any remaining C1, including NEL (U+0085) and the lone introducers.
144
+ // Remaining C1, including NEL (U+0085) and any lone introducer.
141
145
  .replace(/[\u0080-\u009f]/g, ""));
142
146
  }
@@ -47,7 +47,10 @@ export declare function pruneToolExchanges(history: ChatMessage[], keepRecent?:
47
47
  * unanswered: CLI-014's rule holds by construction. The system prompt and the
48
48
  * first user message - the goal - always stay.
49
49
  */
50
- export declare function dropOldestTurns(history: ChatMessage[], keepTurns: number): number;
50
+ export declare function dropOldestTurns(history: ChatMessage[], keepTurns: number): {
51
+ removed: number;
52
+ turns: number;
53
+ };
51
54
  export interface CompactionResult extends PruneResult {
52
55
  /** Whole turns dropped, when exchanges were not enough. */
53
56
  readonly turns: number;
@@ -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,MAAM,CAuBR;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,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,EACxB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CA6BlB"}
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,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,EACxB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CAoClB"}
@@ -125,10 +125,10 @@ export function dropOldestTurns(history, keepTurns) {
125
125
  // The first user message is the goal and is not a droppable turn, so there
126
126
  // have to be more than `keepTurns` turns after it before anything can go.
127
127
  if (userAt.length <= keepTurns + 1)
128
- return 0;
128
+ return { removed: 0, turns: 0 };
129
129
  const cut = userAt[userAt.length - keepTurns];
130
130
  if (cut === undefined)
131
- return 0;
131
+ return { removed: 0, turns: 0 };
132
132
  const kept = [];
133
133
  if (history[0]?.role === "system")
134
134
  kept.push(history[0]);
@@ -138,10 +138,13 @@ export function dropOldestTurns(history, keepTurns) {
138
138
  kept.push(...history.slice(cut));
139
139
  const removed = history.length - kept.length;
140
140
  if (removed <= 0)
141
- return 0;
141
+ return { removed: 0, turns: 0 };
142
142
  history.length = 0;
143
143
  history.push(...kept);
144
- return removed;
144
+ // T-7 · The count of *turns*, not of messages. Reporting a fixed 1 made the
145
+ // display say "dropped 1 earlier turn" after removing eight of them.
146
+ const turns = userAt.filter((at) => at < cut && at !== userAt[0]).length;
147
+ return { removed, turns };
145
148
  }
146
149
  /**
147
150
  * Shorten the history as much as can be done safely.
@@ -156,12 +159,12 @@ export function dropOldestTurns(history, keepTurns) {
156
159
  */
157
160
  export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
158
161
  const pruned = pruneToolExchanges(history, keepRecent);
159
- const turnsRemoved = dropOldestTurns(history, keepRecent);
162
+ const dropped = dropOldestTurns(history, keepRecent);
160
163
  return {
161
- removed: pruned.removed + turnsRemoved,
164
+ removed: pruned.removed + dropped.removed,
162
165
  exchanges: pruned.exchanges,
163
- turns: turnsRemoved > 0 ? 1 : 0,
164
- exhausted: pruned.exchanges === 0 && turnsRemoved === 0,
166
+ turns: dropped.turns,
167
+ exhausted: pruned.exchanges === 0 && dropped.removed === 0,
165
168
  };
166
169
  }
167
170
  /**
@@ -180,11 +183,17 @@ export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
180
183
  */
181
184
  export function compactUntilItFits(history, contextLength, lastPromptTokens, keepRecent = KEEP_RECENT_EXCHANGES) {
182
185
  const startedWith = history.length;
183
- const perMessage = startedWith > 0 ? lastPromptTokens / startedWith : 0;
186
+ const floor = fixedOverhead(history, lastPromptTokens);
187
+ const perMessage = startedWith > 0 ? Math.max(0, lastPromptTokens - floor) / startedWith : 0;
184
188
  let exchanges = 0;
185
189
  let turns = 0;
186
190
  for (;;) {
187
- const estimate = Math.round(perMessage * history.length);
191
+ // T-2 · The floor does not shrink. The system prompt and the tool schemas
192
+ // are re-sent every turn whatever the history looks like, so treating the
193
+ // whole measurement as proportional to the message count said a
194
+ // conversation had come inside the window when only the removable part
195
+ // had. Everything below the floor is not ours to drop.
196
+ const estimate = floor + Math.round(perMessage * history.length);
188
197
  if (!shouldCompact(contextLength, estimate)) {
189
198
  return {
190
199
  removed: startedWith - history.length,
@@ -207,3 +216,29 @@ export function compactUntilItFits(history, contextLength, lastPromptTokens, kee
207
216
  turns += step.turns;
208
217
  }
209
218
  }
219
+ /**
220
+ * T-2 · What the next request costs before any history is added.
221
+ *
222
+ * The system prompt is measurable; the tool schemas are not from here - the
223
+ * loop knows them but this module does not, and CLI-000B found the gateway's
224
+ * own reservation undercounts badly for exactly that reason. So it is
225
+ * approximated from the one message we can size, with a floor that keeps the
226
+ * estimate conservative: over-estimating the fixed part makes compaction try
227
+ * harder, which is the direction that fails safe.
228
+ */
229
+ function fixedOverhead(history, lastPromptTokens) {
230
+ const system = history.find((m) => m.role === "system");
231
+ if (system === undefined || lastPromptTokens <= 0)
232
+ return 0;
233
+ const chars = (system.content ?? "").length;
234
+ // Roughly four characters per token, doubled to stand in for the tool
235
+ // schemas that are sent alongside it and cannot be measured here.
236
+ const approx = Math.round((chars / 4) * 2);
237
+ // Bounded by the measurement, because the fixed part cannot exceed the whole
238
+ // request - but NOT halved. Capping it lower was the unsafe direction: it
239
+ // let the loop conclude that dropping a few messages had brought a history
240
+ // inside a window the system prompt alone did not fit in. Over-estimating
241
+ // makes compaction try harder and, at worst, stop with an honest message;
242
+ // under-estimating sends a request that fails upstream and is billed.
243
+ return Math.min(approx, lastPromptTokens);
244
+ }
@@ -36,6 +36,13 @@ export interface AgentReport {
36
36
  promptTokens: number;
37
37
  completionTokens: number;
38
38
  costMicroUsd: bigint;
39
+ /**
40
+ * T-4 · Requests the cost above does not include, because the catalogue had
41
+ * no price for them. Without it every live summary shows `$0.00` for a run
42
+ * that was not free - the session store learned to say "unknown" and the
43
+ * surfaces the user actually watches did not.
44
+ */
45
+ unpricedRequests: number;
39
46
  /** Set when the run ended because something failed. */
40
47
  error?: string;
41
48
  }
@@ -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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"}
@@ -44,11 +44,22 @@ export async function runAgent(options) {
44
44
  promptTokens: 0,
45
45
  completionTokens: 0,
46
46
  costMicroUsd: 0n,
47
+ unpricedRequests: 0,
47
48
  };
48
49
  /** What the provider counted for the previous request; 0 until one lands. */
49
50
  let lastPromptTokens = 0;
50
51
  /** R-5 · Turns whose cost the catalogue could not tell us. */
51
52
  let unpricedTurns = 0;
53
+ // T-3 · Known before anything is sent. Waiting for the first response to
54
+ // discover it meant a run with an explicit ceiling still made one billable
55
+ // request that could never be counted against it.
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.`);
62
+ }
52
63
  for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
53
64
  if (options.signal?.aborted)
54
65
  return stop(report, "cancelled");
@@ -133,8 +144,10 @@ export async function runAgent(options) {
133
144
  costMicroUsd: turnCost,
134
145
  priced: canPrice(options.pricing, usage),
135
146
  });
136
- if (!canPrice(options.pricing, usage))
147
+ if (!canPrice(options.pricing, usage)) {
137
148
  unpricedTurns += 1;
149
+ report.unpricedRequests += 1;
150
+ }
138
151
  const choice = response.choices?.[0];
139
152
  const message = choice?.message;
140
153
  const calls = message?.tool_calls ?? [];
@@ -32,6 +32,16 @@ export interface SessionOptions {
32
32
  * the same events are drawn.
33
33
  */
34
34
  onEvent?: (event: AgentEvent) => void;
35
+ /**
36
+ * T-1 · The user's own words, which never appear in the agent event stream.
37
+ *
38
+ * Called from the loop rather than from each surface's `readLine`, because
39
+ * that is exactly how it came to be missing: the TUI wrapped its reader and
40
+ * `mafdet agent` did not, so a command-mode session recorded the model's
41
+ * replies and none of the questions - and resuming it could not continue the
42
+ * conversation, because half of it was not there.
43
+ */
44
+ onGoal?: (goal: string) => void;
35
45
  /** R-4 · False when stdout is a pipe or a file, which get exact bytes. */
36
46
  isTty?: boolean;
37
47
  /**
@@ -48,6 +58,8 @@ export interface SessionSummary {
48
58
  promptTokens: number;
49
59
  completionTokens: number;
50
60
  costMicroUsd: bigint;
61
+ /** T-4 · Requests the cost above does not include; see AgentReport. */
62
+ unpricedRequests: number;
51
63
  }
52
64
  /**
53
65
  * The interactive session (plan §4.4).
@@ -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,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;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAqGzB;AAgHD;;;;;;;;;;;;;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;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,6 +1,6 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { isAgentEligible } from "../api/models.js";
3
- import { formatUsd } from "../output/money.js";
3
+ import { formatUsdWithUnpriced } from "../output/money.js";
4
4
  import { renderRecord } from "../output/format.js";
5
5
  import { safeForTerminal } from "../output/terminal-safe.js";
6
6
  import { runAgent } from "./loop.js";
@@ -32,6 +32,7 @@ export async function runSession(options) {
32
32
  promptTokens: 0,
33
33
  completionTokens: 0,
34
34
  costMicroUsd: 0n,
35
+ unpricedRequests: 0,
35
36
  };
36
37
  let model = options.model;
37
38
  let pricing = options.pricing;
@@ -69,6 +70,7 @@ export async function runSession(options) {
69
70
  break;
70
71
  continue;
71
72
  }
73
+ options.onGoal?.(line);
72
74
  summary.goals += 1;
73
75
  const report = await runAgent({
74
76
  client: options.client,
@@ -95,6 +97,7 @@ export async function runSession(options) {
95
97
  summary.promptTokens += report.promptTokens;
96
98
  summary.completionTokens += report.completionTokens;
97
99
  summary.costMicroUsd += report.costMicroUsd;
100
+ summary.unpricedRequests += report.unpricedRequests;
98
101
  // R-4 · The model's answer reaches a terminal here too. The TUI wraps
99
102
  // this writer, but `mafdet agent` without a goal does not, so the
100
103
  // sanitising belongs at the sink rather than at one of its callers.
@@ -180,7 +183,10 @@ async function handleCommand(line, context) {
180
183
  "tokens",
181
184
  `${context.summary.promptTokens} in / ${context.summary.completionTokens} out`,
182
185
  ],
183
- ["cost", `~${formatUsd(context.summary.costMicroUsd)}`],
186
+ [
187
+ "cost",
188
+ `~${formatUsdWithUnpriced(context.summary.costMicroUsd, context.summary.unpricedRequests)}`,
189
+ ],
184
190
  ]));
185
191
  return false;
186
192
  case "/compact": {
@@ -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,CAQxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,SAAS,CAuBX;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;;;;;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":"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,CAQxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,SAAS,CAuBX;AAED,8CAA8C;AAC9C,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CAQ3B;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AA0BD;;;;;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,4 @@
1
+ import { stripAnsi } from "../agent/command-policy.js";
1
2
  /**
2
3
  * Agent admission. Every condition is something the catalogue states outright;
3
4
  * nothing is inferred from the model id, the provider or the display name —
@@ -51,12 +52,37 @@ export function intersectModels(dataPlane, catalog) {
51
52
  /** GET /v1/models — the key's access list. */
52
53
  export async function fetchAccessibleModels(client) {
53
54
  const { data } = await client.getJson("/models");
54
- return data.data ?? [];
55
+ // T-5 · Same reason as `cleanModel`: these ids are printed as the "unlisted"
56
+ // set, so they reach a terminal without ever passing through the catalogue.
57
+ return (data.data ?? []).map((model) => ({
58
+ ...model,
59
+ id: typeof model.id === "string" ? stripAnsi(model.id) : model.id,
60
+ }));
55
61
  }
56
62
  /** GET /models on the business API — public catalogue, no key required. */
57
63
  export async function fetchCatalog(client) {
58
64
  const { data } = await client.getJson("/models");
59
- return data.data ?? [];
65
+ return (data.data ?? []).map(cleanModel);
66
+ }
67
+ /**
68
+ * T-5 · Strip terminal control sequences from catalogue text, once, here.
69
+ *
70
+ * The catalogue is remote data that ends up on a terminal in the picker, in
71
+ * `models list`, in `models show`, in `doctor` and in error messages. F-010
72
+ * was fixed sink by sink across two review rounds and the picker was still
73
+ * missing, which is the argument for doing it where the data enters instead:
74
+ * every renderer downstream, present and future, then has nothing to forget.
75
+ *
76
+ * The model id is cleaned too. It is sent back to the API, so in principle
77
+ * this changes what we would ask for - but an id carrying a control character
78
+ * is not a model id, and declining to echo one back is right either way.
79
+ */
80
+ function cleanModel(model) {
81
+ const clean = (value) => typeof value === "string" ? stripAnsi(value) : value;
82
+ return Object.fromEntries(Object.entries(model).map(([key, value]) => [
83
+ key,
84
+ Array.isArray(value) ? value.map(clean) : clean(value),
85
+ ]));
60
86
  }
61
87
  /**
62
88
  * Check the model serves the endpoint the command needs, before spending a
@@ -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,CAyFvB"}
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"}
@@ -153,14 +153,23 @@ export async function readChatStream(response, events = {}, signal, limits = {})
153
153
  message: `the stream ran for longer than ${totalMs}ms`,
154
154
  });
155
155
  }
156
- const chunk = await withIdleDeadline(reader.read(), idleMs);
156
+ // T-6 · Whichever runs out first. Checking the total before the read
157
+ // and then waiting a whole idle period overshot the total by up to one
158
+ // of them - with a 30 minute total and a 2 minute idle that is a
159
+ // rounding error, but with the total below the idle it never applied
160
+ // at all.
161
+ const remaining = totalMs - (Date.now() - startedAt);
162
+ const chunk = await withIdleDeadline(reader.read(), Math.max(0, Math.min(idleMs, remaining)));
157
163
  if (chunk === IDLE) {
158
164
  limits.abort?.();
159
165
  await reader.cancel().catch(() => { });
166
+ const overTotal = Date.now() - startedAt >= totalMs;
160
167
  throw new ApiError({
161
168
  kind: "timeout",
162
169
  exitCode: ExitCode.Unavailable,
163
- message: `the stream sent nothing for ${idleMs}ms`,
170
+ message: overTotal
171
+ ? `the stream ran for longer than ${totalMs}ms`
172
+ : `the stream sent nothing for ${idleMs}ms`,
164
173
  });
165
174
  }
166
175
  const { done, value } = chunk;
@@ -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;CACrD;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA6HnB;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,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;CACrD;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CAgInB;AAoLD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
@@ -16,7 +16,7 @@ import { WRITE_TOOLS } from "../agent/tools/write.js";
16
16
  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
- import { formatUsd } from "../output/money.js";
19
+ import { formatUsdWithUnpriced } from "../output/money.js";
20
20
  import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
21
21
  const USAGE = `Usage:
22
22
  mafdet agent "<goal>" [--model <id>] [--allow-write] [--read-only]
@@ -127,7 +127,10 @@ export async function agentCommand(argv, io) {
127
127
  "tokens",
128
128
  `${summary.promptTokens} in / ${summary.completionTokens} out`,
129
129
  ],
130
- ["cost", `~${formatUsd(summary.costMicroUsd)}`],
130
+ [
131
+ "cost",
132
+ `~${formatUsdWithUnpriced(summary.costMicroUsd, summary.unpricedRequests)}`,
133
+ ],
131
134
  ]));
132
135
  return ExitCode.Ok;
133
136
  }
@@ -344,7 +347,10 @@ function reportEvent(event, io) {
344
347
  /** Exit codes follow §4.5: a ceiling is 7, a cancel is 6. */
345
348
  function finish(report, json, io) {
346
349
  if (json) {
347
- io.stdout(renderJson({ ...report, costMicroUsd: report.costMicroUsd.toString() }));
350
+ io.stdout(
351
+ // T-4 · `unpricedRequests` rides along in the report, so a consumer can
352
+ // tell a genuinely free run from one nobody could price.
353
+ renderJson({ ...report, costMicroUsd: report.costMicroUsd.toString() }));
348
354
  }
349
355
  else {
350
356
  // R-4 · The model's answer, sanitised only when a terminal is reading it.
@@ -365,7 +371,10 @@ function finish(report, json, io) {
365
371
  "tokens",
366
372
  `${report.promptTokens} in / ${report.completionTokens} out`,
367
373
  ],
368
- ["cost", `~${formatUsd(report.costMicroUsd)}`],
374
+ [
375
+ "cost",
376
+ `~${formatUsdWithUnpriced(report.costMicroUsd, report.unpricedRequests)}`,
377
+ ],
369
378
  ]));
370
379
  if (report.error)
371
380
  io.stderr(`error: ${report.error}\n`);
@@ -351,11 +351,16 @@ async function usage(argv, io, root) {
351
351
  prompt_tokens: m.promptTokens,
352
352
  completion_tokens: m.completionTokens,
353
353
  estimated_charge_micro_usd: m.estimatedMicroUsd.toString(),
354
+ // T-10 · Without this a caller cannot tell whether one model's "0"
355
+ // is free or unpriced, even though the top-level total says some
356
+ // requests were not counted.
357
+ unpriced_requests: m.unpricedRequests,
354
358
  })),
355
359
  by_api_key: totals.byKey.map((k) => ({
356
360
  api_key_ref: k.apiKeyRef,
357
361
  model_requests: k.modelRequests,
358
362
  estimated_charge_micro_usd: k.estimatedMicroUsd.toString(),
363
+ unpriced_requests: k.unpricedRequests,
359
364
  })),
360
365
  ...(refreshed === undefined ? {} : { refresh: jsonRefresh(refreshed) }),
361
366
  }));
@@ -396,7 +401,9 @@ async function usage(argv, io, root) {
396
401
  { header: "REQUESTS", value: (m) => `${m.modelRequests}` },
397
402
  {
398
403
  header: "ESTIMATE",
399
- value: (m) => formatUsd(m.estimatedMicroUsd.toString()),
404
+ value: (m) => m.unpricedRequests > 0
405
+ ? `${formatUsd(m.estimatedMicroUsd.toString())} + ${m.unpricedRequests} unpriced`
406
+ : formatUsd(m.estimatedMicroUsd.toString()),
400
407
  },
401
408
  ]));
402
409
  }
@@ -408,7 +415,9 @@ async function usage(argv, io, root) {
408
415
  { header: "REQUESTS", value: (k) => `${k.modelRequests}` },
409
416
  {
410
417
  header: "ESTIMATE",
411
- value: (k) => formatUsd(k.estimatedMicroUsd.toString()),
418
+ value: (k) => k.unpricedRequests > 0
419
+ ? `${formatUsd(k.estimatedMicroUsd.toString())} + ${k.unpricedRequests} unpriced`
420
+ : formatUsd(k.estimatedMicroUsd.toString()),
412
421
  },
413
422
  ]));
414
423
  }
@@ -13,4 +13,13 @@ export declare function parseMicroUsd(value: string | number | bigint): bigint;
13
13
  export declare function formatUsd(microUsd: string | number | bigint, options?: {
14
14
  minFractionDigits?: number;
15
15
  }): string;
16
+ /**
17
+ * T-4 · A cost that may be incomplete, described rather than rounded to zero.
18
+ *
19
+ * Four surfaces render a running total - the command summary, the TUI summary,
20
+ * `/cost` and the JSON report - and each one showed `$0.00` for a run whose
21
+ * model the catalogue could not price. The session store had already learned
22
+ * to say "unknown"; the places the user actually watches had not.
23
+ */
24
+ export declare function formatUsdWithUnpriced(microUsd: bigint | string, unpricedRequests: number): string;
16
25
  //# sourceMappingURL=money.d.ts.map
@@ -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"}
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"}
@@ -33,3 +33,17 @@ export function formatUsd(microUsd, options = {}) {
33
33
  digits = fraction.slice(0, minDigits);
34
34
  return `${negative ? "-" : ""}$${whole}.${digits}`;
35
35
  }
36
+ /**
37
+ * T-4 · A cost that may be incomplete, described rather than rounded to zero.
38
+ *
39
+ * Four surfaces render a running total - the command summary, the TUI summary,
40
+ * `/cost` and the JSON report - and each one showed `$0.00` for a run whose
41
+ * model the catalogue could not price. The session store had already learned
42
+ * to say "unknown"; the places the user actually watches had not.
43
+ */
44
+ export function formatUsdWithUnpriced(microUsd, unpricedRequests) {
45
+ const shown = formatUsd(typeof microUsd === "bigint" ? microUsd : microUsd);
46
+ if (unpricedRequests <= 0)
47
+ return shown;
48
+ return `${shown} + ${unpricedRequests} unpriced`;
49
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-surface.d.ts","sourceRoot":"","sources":["../../src/tui/agent-surface.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,IAAI,CACpB,cAAc,EACd,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAC1D,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;IACnD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAoFzB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAgBR"}
1
+ {"version":3,"file":"agent-surface.d.ts","sourceRoot":"","sources":["../../src/tui/agent-surface.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,IAAI,CACpB,cAAc,EACd,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAC1D,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;IACnD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CA+EzB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAqBR"}
@@ -6,7 +6,7 @@
6
6
  // different models, tools, approvals or costs, because there is only one
7
7
  // implementation of all four. What differs is the drawing.
8
8
  import { runSession, } from "../agent/session.js";
9
- import { formatUsd } from "../output/money.js";
9
+ import { formatUsdWithUnpriced } from "../output/money.js";
10
10
  import { tuiApprover } from "./approval.js";
11
11
  import { palette } from "./theme.js";
12
12
  import { renderCard, ToolCardLog } from "./tool-card.js";
@@ -99,16 +99,13 @@ export async function runAgentSurface(options) {
99
99
  // Checkpoint between goals rather than per event: the listing only needs
100
100
  // to be current when someone might go and look at it.
101
101
  options.recorder?.checkpoint();
102
- const line = await promptLine(io, "> ");
103
- // The user's own words never appear in the agent event stream, so they
104
- // are recorded here. Slash commands are control, not conversation.
105
- if (line !== undefined &&
106
- line.trim().length > 0 &&
107
- !line.startsWith("/")) {
108
- options.recorder?.goal(line);
109
- }
110
- return line;
102
+ return promptLine(io, "> ");
111
103
  },
104
+ // T-1 · Recording the goal belongs to the session loop now, so both
105
+ // surfaces get it from the same place instead of one remembering.
106
+ ...(options.recorder === undefined
107
+ ? {}
108
+ : { onGoal: (goal) => options.recorder?.goal(goal) }),
112
109
  onEvent,
113
110
  };
114
111
  // Installed after the spread, on purpose - see getSignal above.
@@ -133,7 +130,7 @@ export function renderSummary(summary, capabilities) {
133
130
  dim(` model requests ${summary.modelRequests}`),
134
131
  dim(` tool runs ${summary.toolRuns}`),
135
132
  dim(` tokens ${summary.promptTokens} in / ${summary.completionTokens} out`),
136
- dim(` estimated cost ${formatUsd(summary.costMicroUsd.toString())}`),
133
+ dim(` estimated cost ${formatUsdWithUnpriced(summary.costMicroUsd, summary.unpricedRequests)}`),
137
134
  "",
138
135
  ].join("\n") + "\n");
139
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"AAQA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKrD,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CAQlE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAQR;AAOD,wBAAsB,aAAa,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAoBxB"}
1
+ {"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"AAQA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKrD,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CAQlE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAYR;AAOD,wBAAsB,aAAa,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAoBxB"}
@@ -30,7 +30,11 @@ export function describeSession(entry, capabilities) {
30
30
  stripAnsi(entry.model_id),
31
31
  stripAnsi(workspaceName(entry.workspace)),
32
32
  `${entry.model_requests} request(s)`,
33
- `~${formatUsd(entry.estimated_charge_micro_usd)}`,
33
+ // T-8 · `sessions list/show/usage` learned to say "unknown"; this list
34
+ // did not, so the same session read as free in the picker.
35
+ entry.unpriced_requests
36
+ ? `~${formatUsd(entry.estimated_charge_micro_usd)} + ${entry.unpriced_requests} unpriced`
37
+ : `~${formatUsd(entry.estimated_charge_micro_usd)}`,
34
38
  ];
35
39
  return palette(capabilities).dim(parts.join(" - "));
36
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mafdet/cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Mafdet AI command line — model calls, account queries, and a permissioned local agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",