@cspeach/cli 1.1.1 → 1.1.2

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.
@@ -19,6 +19,8 @@ import { turnStatusEmitter } from '../ui/turn-status-emitter.js';
19
19
  // two-line dispatch renderer; re-add if a future in-place spinner returns)
20
20
  import { buildRetryCapPausePayload, buildSkippedSiblingResults } from './retry-cap.js';
21
21
  import { appendCostLine, buildEntry as buildCostEntry } from '../cost/cost-log.js';
22
+ import { computeSessionCost } from '../cost/session-cost.js';
23
+ import { globalStore } from '../ui/sap-state-store.js';
22
24
  import { lastAssistantIsAwaitingAnswer } from '../session/awaiting-answer.js';
23
25
  import { enrichUserMessage } from '../router/intent-extractor.js';
24
26
  import { resetRule8State, shouldGateRule8 } from '../repl/rule8-detector.js';
@@ -698,6 +700,11 @@ export async function runTurn(params) {
698
700
  session.usage.cache_creation_input_tokens =
699
701
  (session.usage.cache_creation_input_tokens ?? 0)
700
702
  + (msgUsage.cache_creation_input_tokens ?? 0);
703
+ // F-L2-2 — live spend. The store's 200ms trailing rate limiter
704
+ // absorbs stream-event volume; no extra throttling here.
705
+ if (params.pushSpend) {
706
+ globalStore.update({ dollarSpendToday: computeSessionCost(session) });
707
+ }
701
708
  }
702
709
  // New message begins — reset the per-message cumulative-output scratch.
703
710
  messageOutputTokensSoFar = 0;
@@ -720,6 +727,10 @@ export async function runTurn(params) {
720
727
  if (inc > 0) {
721
728
  session.usage.output_tokens += inc;
722
729
  messageOutputTokensSoFar = deltaUsage.output_tokens;
730
+ // F-L2-2 — live spend (see message_start above).
731
+ if (params.pushSpend) {
732
+ globalStore.update({ dollarSpendToday: computeSessionCost(session) });
733
+ }
723
734
  }
724
735
  }
725
736
  }
@@ -76,9 +76,20 @@ export async function verifyAndSpendApprovalId(jwt, expectedObject, expectedOp)
76
76
  // signature-verification failure code. Mismatch reasons (nonce, object,
77
77
  // op) fall out of the post-verify checks below and are returned
78
78
  // verbatim — no catch involved.
79
+ //
80
+ // F-L2-13: normalize the token at the tool boundary. An approval_id that
81
+ // arrives wrapped in stray whitespace (a model-emitted argument with a
82
+ // trailing newline, a copy/paste with leading spaces) is a structurally
83
+ // valid JWT that jose rejects as malformed — the user then sees
84
+ // APPROVAL_INVALID / invalid_signature, which reads as "your token is
85
+ // forged" and offers no way forward. Trimming here covers all 10
86
+ // tool-boundary callers (sap-write.ts, transport.ts) at one site. Nonce
87
+ // bookkeeping keys on the decoded `payload.nonce`, never on the raw
88
+ // string, so trimming cannot desynchronize single-use accounting.
89
+ const token = String(jwt ?? '').trim();
79
90
  let payload;
80
91
  try {
81
- const verified = await jwtVerify(jwt, sessionKey);
92
+ const verified = await jwtVerify(token, sessionKey);
82
93
  payload = verified.payload;
83
94
  }
84
95
  catch (err) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Session-scoped cost total — the single source of truth for the statusline
3
+ * `$` segment and the printed status footer's session line.
4
+ *
5
+ * F-L2-2 (2026-08-11): this computation was inlined in
6
+ * `repl/post-turn-status.ts`, the POST-turn chokepoint. That meant the
7
+ * statusline spend only ever moved between turns — during a long turn (the
8
+ * exact moment a user wants to watch the meter) it sat frozen. Extracted
9
+ * here verbatim so the agent loop can call it mid-stream (after each
10
+ * `message_start` / `message_delta` usage mutation) and push the running
11
+ * total to the store.
12
+ *
13
+ * Honest-semantics caveat, carried over unchanged from the original site:
14
+ * the total is priced at `session.model` over LIFETIME `session.usage`. On a
15
+ * session with model-tiered turns (A4 modelOverride) that is a mixed-model
16
+ * approximation — the per-turn cost JSONL records the model that actually
17
+ * ran. Same caveat as the footer's session line.
18
+ */
19
+ import { computeCost } from './pricing.js';
20
+ /** USD cost of the session's lifetime usage, priced at `session.model`. */
21
+ export function computeSessionCost(session) {
22
+ return computeCost(session.model, {
23
+ input: session.usage.input_tokens,
24
+ output: session.usage.output_tokens,
25
+ cacheRead: session.usage.cache_read_input_tokens,
26
+ cacheCreate: session.usage.cache_creation_input_tokens ?? 0,
27
+ });
28
+ }
@@ -7,6 +7,7 @@
7
7
  * ```abap → existing ABAP regex logic (unchanged)
8
8
  * ```cds → cli-highlight CDS grammar (via syntax.ts)
9
9
  * ```bdef → cli-highlight BDEF grammar (via syntax.ts)
10
+ * ```ddl → cli-highlight CDS grammar (ddl is a CDS alias; via syntax.ts)
10
11
  * ```<any other or empty> → pass through (no highlight)
11
12
  *
12
13
  * The function is renamed `applyCodeHighlight`. A deprecation re-export
@@ -64,7 +65,7 @@ export function applyCodeHighlight(text) {
64
65
  // dependency for the hottest fenced-block type.
65
66
  out.push(highlightAbapBlock(body));
66
67
  }
67
- else if (lang === 'cds' || lang === 'bdef') {
68
+ else if (lang === 'cds' || lang === 'bdef' || lang === 'ddl') {
68
69
  // cli-highlight grammars via the Task 5 dispatcher.
69
70
  out.push(highlightCodeBlock(body, lang));
70
71
  }
@@ -47,7 +47,13 @@ const hljs = createRequire(require.resolve('cli-highlight'))('highlight.js');
47
47
  hljs.registerLanguage('abap', () => abapGrammar);
48
48
  hljs.registerLanguage('cds', () => cdsGrammar);
49
49
  hljs.registerLanguage('bdef', () => bdefGrammar);
50
- const SUPPORTED = new Set(['abap', 'cds', 'bdef']);
50
+ // `ddl` is an alias of the CDS grammar: ```ddl fences carry CDS DDL source
51
+ // (the abap-rap templates emit them). Registering it on the same hljs
52
+ // instance also silences marked-terminal's "Could not find the language
53
+ // 'ddl'" warning, since marked-terminal calls cli-highlight directly with
54
+ // the raw fence lang and never goes through highlightCodeBlock().
55
+ hljs.registerLanguage('ddl', () => cdsGrammar);
56
+ const SUPPORTED = new Set(['abap', 'cds', 'bdef', 'ddl']);
51
57
  /**
52
58
  * Highlight a code block body (without fence markers).
53
59
  *
@@ -15,12 +15,23 @@
15
15
  * Module-scope singleton per CLI process. Tests must call clearCurrentTransport()
16
16
  * in beforeEach to avoid leakage across Vitest test files.
17
17
  *
18
- * Task 9 (ux-wave1) — this setter is ALSO the single statusline wire for the
18
+ * Task 9 (ux-wave1) — this setter is ALSO a statusline wire for the
19
19
  * `tr <TRKORR>` segment: every path that changes the session transport
20
20
  * (sap_transport create success, owning-transport adoption in the create
21
21
  * guard, /transport slash command) funnels through set/clear here, so the
22
22
  * globalStore push lives HERE rather than scattered across tool handlers.
23
23
  * Store writes are harmless in classic mode (nothing subscribes there).
24
+ *
25
+ * F-L2-1 (2026-08-11) — that wire alone is not enough, so this module now
26
+ * carries a SECOND, write-side wire: noteWriteTransport(). The session flow
27
+ * above is the only writer of `store.transport`, but a per-write `corrNr`
28
+ * coming from a prompt or a plan never travels through it — live evidence: a
29
+ * whole session's writes stamped S4HK903635 while the statusline read
30
+ * `tr none`. noteWriteTransport records what a write ACTUALLY stamped into a
31
+ * SEPARATE store field (`lastWriteTransport`) that the statusline falls back
32
+ * to. It deliberately does NOT touch the session pointer below: that pointer
33
+ * feeds the 3-tier write-transport fallback, and moving it would change write
34
+ * behaviour, not just the display.
24
35
  */
25
36
  import { globalStore } from '../ui/sap-state-store.js';
26
37
  let currentTransport = null;
@@ -35,6 +46,17 @@ export function clearCurrentTransport() {
35
46
  currentTransport = null;
36
47
  globalStore.update({ transport: null });
37
48
  }
49
+ /**
50
+ * Statusline-only wire (F-L2-1): record the transport actually stamped on the
51
+ * last successful write. Never touches the session transport pointer — see the
52
+ * module doc. No-op on a falsy trkorr (local / $TMP objects write without one,
53
+ * and blanking the display there would lose the last real datapoint).
54
+ */
55
+ export function noteWriteTransport(trkorr) {
56
+ if (!trkorr)
57
+ return;
58
+ globalStore.update({ lastWriteTransport: trkorr });
59
+ }
38
60
  /**
39
61
  * Handles the `/transport [<trkorr>|clear]` slash command. Returns user-facing
40
62
  * status text. Mirrors the shape of handleSafetyModeCommand in safety-mode-state.ts.
@@ -18,6 +18,7 @@
18
18
  * primary per-turn status surface.
19
19
  */
20
20
  import { computeCost } from '../cost/pricing.js';
21
+ import { computeSessionCost } from '../cost/session-cost.js';
21
22
  import { globalStore } from '../ui/sap-state-store.js';
22
23
  import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
23
24
  import { shouldUseInk } from '../renderer/tty.js';
@@ -43,12 +44,9 @@ export function pushPostTurnStatus(params) {
43
44
  cacheCreate: (session.usage.cache_creation_input_tokens ?? 0) - snapshot.cacheCreate,
44
45
  };
45
46
  const turnCost = computeCost(turnModel ?? session.model, turnCostTokens);
46
- const sessionCost = computeCost(session.model, {
47
- input: session.usage.input_tokens,
48
- output: session.usage.output_tokens,
49
- cacheRead: session.usage.cache_read_input_tokens,
50
- cacheCreate: session.usage.cache_creation_input_tokens ?? 0,
51
- });
47
+ // F-L2-2: extracted to cost/session-cost.ts so the agent loop can push the
48
+ // SAME total mid-stream. This call site is unchanged in behaviour.
49
+ const sessionCost = computeSessionCost(session);
52
50
  // Task 9 (ux-wave1) — statusline spend wire. Honest semantics note: the
53
51
  // store field is named `dollarSpendToday` (legacy sidebar naming, kept to
54
52
  // avoid rename churn) but the value written is SESSION-scoped — the same
@@ -56,6 +54,11 @@ export function pushPostTurnStatus(params) {
56
54
  // lifetime session.usage at session.model rates (mixed-model turns are an
57
55
  // approximation, same caveat as the footer's session line). There is no
58
56
  // day-spanning ledger; the statusline shows session spend.
57
+ //
58
+ // F-L2-2 (2026-08-11): the agent loop now pushes the same value mid-stream
59
+ // so the segment ticks DURING a turn, not only between turns. This push
60
+ // stays as the authoritative end-of-turn settle (the loop's pushes are
61
+ // gated to REPL turns; this one runs on every path that reaches here).
59
62
  globalStore.update({ dollarSpendToday: sessionCost });
60
63
  const statusPayload = {
61
64
  sessionId: session.id,
package/dist/repl.js CHANGED
@@ -822,6 +822,7 @@ export async function runRepl(opts) {
822
822
  skill: rr.skill,
823
823
  ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
824
824
  chunkEmitter,
825
+ pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
825
826
  });
826
827
  }
827
828
  finally {
@@ -1254,6 +1255,7 @@ export async function runRepl(opts) {
1254
1255
  skill: skill,
1255
1256
  ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
1256
1257
  chunkEmitter,
1258
+ pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
1257
1259
  signal: currentTurnAbort.signal,
1258
1260
  suppressSaveHook: planResume !== null || planContinue !== null,
1259
1261
  // A4 — undefined on every non-plan turn (and when tiering is off).
@@ -2270,6 +2272,7 @@ export async function runRepl(opts) {
2270
2272
  userMessage: rr.prompt,
2271
2273
  skill: rr.skill,
2272
2274
  ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
2275
+ pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
2273
2276
  });
2274
2277
  continue;
2275
2278
  }
@@ -2797,6 +2800,7 @@ export async function runRepl(opts) {
2797
2800
  userMessage: prompt,
2798
2801
  skill: skill,
2799
2802
  ctx: { adt, sapAlias: selectedAlias, session, cwd: process.cwd(), provider, skillSource: provider.skillSource, previewHook: replPreviewHook, chunkEmitter: classicOutputEmitter, todoEmitter, currentTransport: currentTransportAccessor, pendingDispatch: pendingDispatchAccessor },
2803
+ pushSpend: true, // F-L2-2 — top-level REPL turn: live statusline spend
2800
2804
  signal: turnAbort.signal,
2801
2805
  suppressSaveHook: planResume !== null || planContinue !== null,
2802
2806
  // A4 — undefined on every non-plan turn (and when tiering is off).