@promptctl/cc-candybar 1.42.1 → 1.43.0
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/dist/index.mjs +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- package/src/version.ts +0 -17
package/src/render/strip.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Strip,
|
|
3
|
-
RichText,
|
|
4
|
-
Style,
|
|
5
|
-
PowerlineJoiner,
|
|
6
|
-
CapsuleJoiner,
|
|
7
|
-
PlainJoiner,
|
|
8
|
-
FlexStrip,
|
|
9
|
-
renderToString,
|
|
10
|
-
type Joiner,
|
|
11
|
-
type PowerlineJoinerOptions,
|
|
12
|
-
type CapsuleJoinerOptions,
|
|
13
|
-
} from "@promptctl/rich-js";
|
|
14
|
-
import type {
|
|
15
|
-
Charset,
|
|
16
|
-
ColorCompatibility,
|
|
17
|
-
StripStyle,
|
|
18
|
-
} from "../themes/policy.js";
|
|
19
|
-
|
|
20
|
-
export interface RenderedSegmentLike {
|
|
21
|
-
type: string;
|
|
22
|
-
text: string;
|
|
23
|
-
bgHex?: string;
|
|
24
|
-
fgHex?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// [LAW:one-source-of-truth] `StripStyle`/`Charset` and their value lists live
|
|
28
|
-
// in themes/policy.ts (the render-identifier policy module, importable by the
|
|
29
|
-
// option-source machinery without a render→template-engine cycle). Re-exported
|
|
30
|
-
// here so render-layer consumers can keep importing them from the strip module.
|
|
31
|
-
export type { Charset, ColorCompatibility, StripStyle };
|
|
32
|
-
|
|
33
|
-
// [LAW:one-source-of-truth] Raw terminal cols we assume when the wire
|
|
34
|
-
// didn't give us one (older client, env-stripped spawn). RAW — not
|
|
35
|
-
// post-reserve — so the Claude-Code-UI reserve applies uniformly across
|
|
36
|
-
// wire and fallback paths (callers thread this through
|
|
37
|
-
// applyClaudeCodeReserve from src/utils/terminal-width).
|
|
38
|
-
export const DEFAULT_TERMINAL_WIDTH = 120;
|
|
39
|
-
|
|
40
|
-
// [LAW:one-source-of-truth] autoWrap's and padding's defaults live in
|
|
41
|
-
// themes/policy.ts beside their resolvers and padding's range, because the
|
|
42
|
-
// config loader needs them too and config must not import render
|
|
43
|
-
// [LAW:one-way-deps]. Re-exported here on the same terms as the types above, so
|
|
44
|
-
// render-layer consumers keep importing them from the strip module.
|
|
45
|
-
export { DEFAULT_WRAP, DEFAULT_PADDING } from "../themes/policy.js";
|
|
46
|
-
|
|
47
|
-
// [LAW:one-source-of-truth] The one statement of the globals.charset default
|
|
48
|
-
// (powerline unicode glyphs — current behavior, matching the legacy
|
|
49
|
-
// display.charset). Every resolver of the config global
|
|
50
|
-
// (`globals.charset ?? DEFAULT_CHARSET`) derives from this constant.
|
|
51
|
-
export const DEFAULT_CHARSET: Charset = "unicode";
|
|
52
|
-
|
|
53
|
-
// [LAW:one-source-of-truth] The one statement of the globals.colorCompatibility
|
|
54
|
-
// default (truecolor — the CURRENT pinned value, deliberately NOT the legacy
|
|
55
|
-
// "auto" default, which would change rendering for existing users). Every
|
|
56
|
-
// resolver of the config global (`globals.colorCompatibility ??
|
|
57
|
-
// DEFAULT_COLOR_COMPATIBILITY`) derives from this constant.
|
|
58
|
-
export const DEFAULT_COLOR_COMPATIBILITY: ColorCompatibility = "truecolor";
|
|
59
|
-
|
|
60
|
-
export interface BuildLineOptions {
|
|
61
|
-
style: StripStyle;
|
|
62
|
-
// [LAW:types-are-the-program] Narrower than rich-js ColorSystemSpec on
|
|
63
|
-
// purpose: the four explicit depths only. "auto"/null never reach a render —
|
|
64
|
-
// the daemon is detached, so env detection would read the wrong terminal;
|
|
65
|
-
// the loader rejects "auto" at the trust boundary (see COLOR_COMPATIBILITIES
|
|
66
|
-
// in themes/policy.ts), and every construction site states a resolved depth.
|
|
67
|
-
colorCompatibility: ColorCompatibility;
|
|
68
|
-
separator?: string;
|
|
69
|
-
// [LAW:types-are-the-program] Every render carries a width. Required (not
|
|
70
|
-
// optional) so callers cannot silently drop the wire's value.
|
|
71
|
-
// [LAW:one-source-of-truth] Width is a FACT (usable cells) feeding two
|
|
72
|
-
// consumers — FlexStrip's wrap limit AND the picker's pagination
|
|
73
|
-
// (`term.cols`). It stays finite even when wrapping is off; `wrap` below is
|
|
74
|
-
// the separate POLICY of whether rows may soft-break at that width.
|
|
75
|
-
width: number;
|
|
76
|
-
// [LAW:types-are-the-program] Required for the same reason as width: the
|
|
77
|
-
// wrap decision (globals.autoWrap, default on) must reach every render
|
|
78
|
-
// explicitly — encoding "no wrap" as width=Infinity would corrupt the
|
|
79
|
-
// picker's pagination, which reads the same width value.
|
|
80
|
-
wrap: boolean;
|
|
81
|
-
// [LAW:one-source-of-truth] Spaces inside each segment cell per side
|
|
82
|
-
// (globals.padding, default 1 — the legacy display.padding, intra-cell,
|
|
83
|
-
// not rich-js FlexStrip's inter-item gap). Required so every construction
|
|
84
|
-
// site states the resolved value; the cell builders derive from it and
|
|
85
|
-
// never re-default.
|
|
86
|
-
padding: number;
|
|
87
|
-
// [LAW:one-source-of-truth] Which glyph vocabulary the joiners render with
|
|
88
|
-
// (globals.charset, default "unicode" — the legacy display.charset).
|
|
89
|
-
// Required for the same reason as padding: the resolved value reaches every
|
|
90
|
-
// render explicitly; pickJoiner derives from it and never re-defaults.
|
|
91
|
-
charset: Charset;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// [LAW:dataflow-not-control-flow] Charset variability lives in these VALUES,
|
|
95
|
-
// not in branches: per style, each charset names the joiner-construction
|
|
96
|
-
// options. The unicode entries are empty on purpose — rich-js owns its
|
|
97
|
-
// canonical powerline glyphs (U+E0B0 / U+E0B6+U+E0B4) and restating them here
|
|
98
|
-
// would be a second source that could drift [LAW:one-source-of-truth]. The
|
|
99
|
-
// ascii glyphs are DELIBERATELY single-column (same display width as the
|
|
100
|
-
// unicode caps) so stripChromeCols stays charset-invariant; the
|
|
101
|
-
// measured-chrome pin in test/picker-pagination.test.ts checks that for every
|
|
102
|
-
// style × charset. Widen a glyph and that pin fails loudly.
|
|
103
|
-
const POWERLINE_GLYPHS: Record<Charset, PowerlineJoinerOptions> = {
|
|
104
|
-
unicode: {},
|
|
105
|
-
ascii: { glyph: ">" },
|
|
106
|
-
};
|
|
107
|
-
const CAPSULE_GLYPHS: Record<Charset, CapsuleJoinerOptions> = {
|
|
108
|
-
unicode: {},
|
|
109
|
-
ascii: { left: "(", right: ")" },
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
function pickJoiner(
|
|
113
|
-
style: StripStyle,
|
|
114
|
-
charset: Charset,
|
|
115
|
-
separator?: string,
|
|
116
|
-
): Joiner {
|
|
117
|
-
// [LAW:dataflow-not-control-flow] joiner choice is data-driven; one arm per
|
|
118
|
-
// shape. Style picks the joiner CLASS, charset indexes the glyph options fed
|
|
119
|
-
// to it — the two dimensions stay orthogonal (any style renders under either
|
|
120
|
-
// charset). Plain takes no charset lookup: its separator is already user
|
|
121
|
-
// data (globals.default_separator) and its default (" | ") is ASCII-safe.
|
|
122
|
-
// [LAW:types-are-the-program] Total over StripStyle — the `never`
|
|
123
|
-
// default makes adding a STRIP_STYLES member a compile error here until it
|
|
124
|
-
// gets a joiner, so the picker's domain can never offer an unrenderable shape.
|
|
125
|
-
switch (style) {
|
|
126
|
-
case "capsule":
|
|
127
|
-
return new CapsuleJoiner(CAPSULE_GLYPHS[charset]);
|
|
128
|
-
case "plain":
|
|
129
|
-
return new PlainJoiner(separator !== undefined ? { separator } : {});
|
|
130
|
-
case "powerline":
|
|
131
|
-
return new PowerlineJoiner(POWERLINE_GLYPHS[charset]);
|
|
132
|
-
default: {
|
|
133
|
-
const _exhaustive: never = style;
|
|
134
|
-
return _exhaustive;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// [LAW:single-enforcer] Strip geometry has one owner — this module builds every
|
|
140
|
-
// joiner (pickJoiner) and so alone knows the structural chrome a styled row costs
|
|
141
|
-
// beyond its content: the joiner's end-caps, which FlexStrip paints OUTSIDE the
|
|
142
|
-
// width budget. A single full-width row's content can occupy only `width - chrome`
|
|
143
|
-
// before the caps push the line past `width`. Returned per style so a width-fit
|
|
144
|
-
// widget (the picker) can reserve it and never overflow the wrapped line.
|
|
145
|
-
//
|
|
146
|
-
// [LAW:dataflow-not-control-flow] / [LAW:types-are-the-program] Total over
|
|
147
|
-
// StripStyle — the `never` default makes adding a STRIP_STYLES member a compile
|
|
148
|
-
// error here until its chrome is declared, the same guard pickJoiner carries. The
|
|
149
|
-
// numbers are the cap glyphs pickJoiner constructs: powerline appends ONE
|
|
150
|
-
// trailing separator (1 col); capsule brackets BOTH edges (2 cols); plain has no
|
|
151
|
-
// caps. Charset does NOT change these — both glyph vocabularies use
|
|
152
|
-
// single-column caps by construction (see the glyph tables above), which is why
|
|
153
|
-
// this stays total over StripStyle alone. test/picker-pagination.test.ts
|
|
154
|
-
// measures the real rendered chrome against these for every style × charset so
|
|
155
|
-
// the declaration cannot drift from rich-js or from the ascii glyph choice.
|
|
156
|
-
export function stripChromeCols(style: StripStyle): number {
|
|
157
|
-
switch (style) {
|
|
158
|
-
case "powerline":
|
|
159
|
-
return 1;
|
|
160
|
-
case "capsule":
|
|
161
|
-
return 2;
|
|
162
|
-
case "plain":
|
|
163
|
-
return 0;
|
|
164
|
-
default: {
|
|
165
|
-
const _exhaustive: never = style;
|
|
166
|
-
return _exhaustive;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function toCell(seg: RenderedSegmentLike, padding: number): RichText {
|
|
172
|
-
// [LAW:one-source-of-truth] Intra-cell padding derives from the one resolved
|
|
173
|
-
// globals.padding value on BuildLineOptions — the joiners sit between cells;
|
|
174
|
-
// padding sits inside, inheriting the cell's wrapping style (bg fill).
|
|
175
|
-
const style = new Style({
|
|
176
|
-
bgcolor: seg.bgHex || undefined,
|
|
177
|
-
color: seg.fgHex || undefined,
|
|
178
|
-
});
|
|
179
|
-
return new RichText(seg.text, { style, end: "", noWrap: true }).pad(padding);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* [LAW:single-enforcer] The one place RichText cells become an ANSI byte
|
|
184
|
-
* string. Every render path (DSL RichText[] via the template-engine
|
|
185
|
-
* pipeline, buildLineStrip's input-shape adapter, debug per-segment
|
|
186
|
-
* serialization) flows through here. The wrap dispatch lives here too:
|
|
187
|
-
* wrap enabled at a finite width → FlexStrip (rich-js owns the wrap
|
|
188
|
-
* algebra); otherwise → Strip, one unbounded line.
|
|
189
|
-
*
|
|
190
|
-
* [LAW:dataflow-not-control-flow] The dispatch is on values, not on caller
|
|
191
|
-
* branches: `wrap` (the globals.autoWrap policy) gates whether the finite
|
|
192
|
-
* width acts as a break limit. An infinite width has nothing to break at,
|
|
193
|
-
* so it renders unbounded regardless of `wrap`.
|
|
194
|
-
*/
|
|
195
|
-
export function renderStripCells(
|
|
196
|
-
cells: readonly RichText[],
|
|
197
|
-
options: BuildLineOptions,
|
|
198
|
-
): string {
|
|
199
|
-
if (cells.length === 0) return "";
|
|
200
|
-
const joiner = pickJoiner(options.style, options.charset, options.separator);
|
|
201
|
-
if (options.wrap && Number.isFinite(options.width)) {
|
|
202
|
-
const flex = new FlexStrip([...cells], { joiner });
|
|
203
|
-
const out = renderToString(flex, {
|
|
204
|
-
width: options.width,
|
|
205
|
-
colorSystem: options.colorCompatibility,
|
|
206
|
-
});
|
|
207
|
-
return out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
208
|
-
}
|
|
209
|
-
const strip = new Strip([...cells], joiner);
|
|
210
|
-
return renderToString(strip, {
|
|
211
|
-
colorSystem: options.colorCompatibility,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Input-shape adapter for callers that hold RenderedSegmentLike[] rather
|
|
217
|
-
* than pre-constructed RichText cells. Wrap behavior is identical to
|
|
218
|
-
* renderStripCells — the cell construction is the only difference.
|
|
219
|
-
*/
|
|
220
|
-
export function buildLineStrip(
|
|
221
|
-
segments: readonly RenderedSegmentLike[],
|
|
222
|
-
options: BuildLineOptions,
|
|
223
|
-
): string {
|
|
224
|
-
return renderStripCells(
|
|
225
|
-
segments.map((seg) => toCell(seg, options.padding)),
|
|
226
|
-
options,
|
|
227
|
-
);
|
|
228
|
-
}
|
package/src/segments/cache.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// Prompt-cache warmth provider.
|
|
2
|
-
//
|
|
3
|
-
// Anthropic's prompt cache has a fixed TTL (1h): each turn that reads or
|
|
4
|
-
// creates cache entries refreshes it, and after the TTL the next turn pays
|
|
5
|
-
// full cache-creation cost again. This provider answers one question — when
|
|
6
|
-
// does the current session's cache go cold? — by tail-reading the transcript
|
|
7
|
-
// for the most recent entry that touched the cache and projecting its
|
|
8
|
-
// timestamp forward by the TTL.
|
|
9
|
-
//
|
|
10
|
-
// [LAW:dataflow-not-control-flow] The datum is a single epoch instant, not a
|
|
11
|
-
// rendered string. Whether the timer shows "12m", "cold", or hides entirely,
|
|
12
|
-
// and what color it takes, are all functions of this one number evaluated in
|
|
13
|
-
// the DSL template — the same shape block/weekly use with `resetsAt`. The
|
|
14
|
-
// provider carries no display policy.
|
|
15
|
-
//
|
|
16
|
-
// [LAW:types-are-the-program] The return is `Outcome<number>`: a known expiry
|
|
17
|
-
// instant (`ok`), "no cache activity found" (`absent` — no transcript yet, or
|
|
18
|
-
// no cache-bearing entry), or a real read failure (`failed` — the transcript
|
|
19
|
-
// exists but couldn't be read). Absent becomes a missing payload field, which
|
|
20
|
-
// the segment's `when` predicate reads as hidden; failed reaches the payload
|
|
21
|
-
// boundary, the one place that logs it — there is no "0 means hidden"
|
|
22
|
-
// ambiguity to defend against downstream, and no failure dressed as absence.
|
|
23
|
-
|
|
24
|
-
import { ABSENT, ok, type Outcome } from "../utils/outcome.js";
|
|
25
|
-
import { readTail } from "../utils/transcript-fs.js";
|
|
26
|
-
|
|
27
|
-
// Anthropic prompt cache TTL. A const, not a knob: it is a property of the
|
|
28
|
-
// upstream cache, not of this renderer. If a future cache tier ships a
|
|
29
|
-
// different TTL, that is a new arm here, not a user config field.
|
|
30
|
-
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
31
|
-
const TAIL_CHUNK = 64 * 1024;
|
|
32
|
-
const TAIL_MAX = 1 * 1024 * 1024;
|
|
33
|
-
|
|
34
|
-
// [LAW:types-are-the-program] A cheap CANDIDATE filter, not the authority. It
|
|
35
|
-
// matches any line mentioning a non-zero cache-token field, which includes a
|
|
36
|
-
// line whose *message content* merely quotes the string (a pasted JSON snippet,
|
|
37
|
-
// a transcript of a review discussing these very fields). The authoritative
|
|
38
|
-
// check is the parsed `message.usage` value — the regex only avoids JSON.parsing
|
|
39
|
-
// every line; a match is verified before its timestamp is trusted. The `[1-9]`
|
|
40
|
-
// rejects the `":0` common case so most non-cache lines never reach the parser.
|
|
41
|
-
const CACHE_HIT_RE =
|
|
42
|
-
/"(?:cache_read_input_tokens|cache_creation_input_tokens)":[1-9]/;
|
|
43
|
-
|
|
44
|
-
// The transcript-line shape this provider reads. Untrusted JSON — every field is
|
|
45
|
-
// optional and narrowed at use; only positive `message.usage` cache tokens count.
|
|
46
|
-
interface UsageLine {
|
|
47
|
-
readonly timestamp?: string;
|
|
48
|
-
readonly message?: {
|
|
49
|
-
readonly usage?: {
|
|
50
|
-
readonly cache_read_input_tokens?: number;
|
|
51
|
-
readonly cache_creation_input_tokens?: number;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Parse a candidate line and return its millisecond timestamp ONLY if its
|
|
57
|
-
// `message.usage` actually records positive cache activity. A content-only
|
|
58
|
-
// mention (or unparseable line) yields null, so a false-positive regex match
|
|
59
|
-
// can never set the timer warm.
|
|
60
|
-
function cacheActivityTs(line: string): number | null {
|
|
61
|
-
let parsed: UsageLine;
|
|
62
|
-
try {
|
|
63
|
-
parsed = JSON.parse(line) as UsageLine;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const usage = parsed.message?.usage;
|
|
68
|
-
const positive =
|
|
69
|
-
(usage?.cache_read_input_tokens ?? 0) > 0 ||
|
|
70
|
-
(usage?.cache_creation_input_tokens ?? 0) > 0;
|
|
71
|
-
if (!positive) return null;
|
|
72
|
-
const ms = parsed.timestamp != null ? Date.parse(parsed.timestamp) : NaN;
|
|
73
|
-
return Number.isNaN(ms) ? null : ms;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Epoch *seconds* at which the session's prompt cache expires; `absent` when
|
|
78
|
-
* no cache-bearing transcript entry can be found; `failed` when the
|
|
79
|
-
* transcript exists but couldn't be read. Seconds (not millis) to match the
|
|
80
|
-
* unit of block/weekly `resetsAt`, so the DSL composes
|
|
81
|
-
* `minutesUntilReset .cache.expiresAt` with no unit translation.
|
|
82
|
-
*/
|
|
83
|
-
export async function cacheExpiresAt(
|
|
84
|
-
transcriptPath: string,
|
|
85
|
-
): Promise<Outcome<number>> {
|
|
86
|
-
const lastCacheMs = await findLastCacheActivityTs(transcriptPath);
|
|
87
|
-
if (lastCacheMs.kind !== "ok") return lastCacheMs;
|
|
88
|
-
return ok(Math.floor((lastCacheMs.value + CACHE_TTL_MS) / 1000));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Tail-read the JSONL transcript and return the millisecond timestamp of the
|
|
92
|
-
// last entry with cache activity. The relevant entry is almost always within
|
|
93
|
-
// the final few KB, so the common case reads one TAIL_CHUNK; only a transcript
|
|
94
|
-
// whose last cache hit is deeper grows to TAIL_MAX. [LAW:single-enforcer] both
|
|
95
|
-
// reads go through the gated transcript-fs seam (readTail), so this scanner is
|
|
96
|
-
// bounded with every other transcript read instead of blocking the event loop
|
|
97
|
-
// on synchronous fs.
|
|
98
|
-
async function findLastCacheActivityTs(
|
|
99
|
-
transcriptPath: string,
|
|
100
|
-
): Promise<Outcome<number>> {
|
|
101
|
-
for (const maxBytes of [TAIL_CHUNK, TAIL_MAX]) {
|
|
102
|
-
const tail = await readTail(transcriptPath, maxBytes);
|
|
103
|
-
// absent (no transcript yet) and failed (unreadable) both end the scan;
|
|
104
|
-
// the outcome carries which one happened to the payload boundary.
|
|
105
|
-
if (tail.kind !== "ok") return tail;
|
|
106
|
-
const ts = scanBufferForLastCacheTs(tail.value.buf, tail.value.fromStart);
|
|
107
|
-
if (ts != null) return ok(ts);
|
|
108
|
-
// The window reached the file start: the whole transcript is scanned, no
|
|
109
|
-
// hit exists — growing further would re-read the same bytes.
|
|
110
|
-
if (tail.value.fromStart) return ABSENT;
|
|
111
|
-
}
|
|
112
|
-
return ABSENT;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function scanBufferForLastCacheTs(
|
|
116
|
-
buf: Buffer,
|
|
117
|
-
bufStartsAtFileBeginning: boolean,
|
|
118
|
-
): number | null {
|
|
119
|
-
const text = buf.toString("utf8");
|
|
120
|
-
const lines = text.split("\n");
|
|
121
|
-
// When the window doesn't start at the file beginning, the first line is
|
|
122
|
-
// likely a partial JSON object — skip it so we never mis-parse a fragment.
|
|
123
|
-
const start = bufStartsAtFileBeginning ? 0 : 1;
|
|
124
|
-
for (let i = lines.length - 1; i >= start; i--) {
|
|
125
|
-
const line = lines[i];
|
|
126
|
-
if (!line || !CACHE_HIT_RE.test(line)) continue; // cheap candidate filter
|
|
127
|
-
const ts = cacheActivityTs(line); // authoritative: parsed usage must be > 0
|
|
128
|
-
if (ts != null) return ts;
|
|
129
|
-
}
|
|
130
|
-
return null;
|
|
131
|
-
}
|
package/src/segments/context.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import type { ParsedEntry, ClaudeHookData } from "../utils/claude";
|
|
2
|
-
|
|
3
|
-
import { debug } from "../utils/logger";
|
|
4
|
-
import { parseJsonlFile } from "../utils/claude";
|
|
5
|
-
import { ABSENT, failed, ok, type Outcome } from "../utils/outcome";
|
|
6
|
-
|
|
7
|
-
export interface ContextInfo {
|
|
8
|
-
totalTokens: number;
|
|
9
|
-
// Used / remaining percentages. Sourced from Claude's native
|
|
10
|
-
// context_window.used_percentage / remaining_percentage when present; a
|
|
11
|
-
// plain token-ratio is the only fallback (no auto-compact buffer guess).
|
|
12
|
-
percentage: number;
|
|
13
|
-
contextLeftPercentage: number;
|
|
14
|
-
maxTokens: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ContextUsageThresholds {
|
|
18
|
-
LOW: number;
|
|
19
|
-
MEDIUM: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// [LAW:one-source-of-truth] The context-window size is NEVER guessed from the
|
|
23
|
-
// model name. Claude Code reports the real size for the active model in
|
|
24
|
-
// `context_window.context_window_size` (1M for the [1m] variants, 200k
|
|
25
|
-
// otherwise) — that field is the single authority. This constant is the
|
|
26
|
-
// last-resort floor for ancient clients that omit `context_window` entirely;
|
|
27
|
-
// it is not a per-model table and must not grow into one.
|
|
28
|
-
const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
29
|
-
|
|
30
|
-
export class ContextProvider {
|
|
31
|
-
private readonly thresholds: ContextUsageThresholds = {
|
|
32
|
-
LOW: 50,
|
|
33
|
-
MEDIUM: 80,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
getContextUsageThresholds(): ContextUsageThresholds {
|
|
37
|
-
return this.thresholds;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Token-ratio percentages — the fallback ONLY. Used when Claude doesn't
|
|
41
|
-
// report used_percentage / remaining_percentage natively (transcript path,
|
|
42
|
-
// or a native window whose percentages are still null pre-first-call). No
|
|
43
|
-
// auto-compact buffer: that was a hardcoded guess at Claude's threshold and
|
|
44
|
-
// a soft second source; the native remaining_percentage is authoritative.
|
|
45
|
-
private ratioPercentages(
|
|
46
|
-
totalTokens: number,
|
|
47
|
-
contextLimit: number,
|
|
48
|
-
): Pick<ContextInfo, "percentage" | "contextLeftPercentage"> {
|
|
49
|
-
const percentage = Math.min(
|
|
50
|
-
100,
|
|
51
|
-
Math.max(0, Math.round((totalTokens / contextLimit) * 100)),
|
|
52
|
-
);
|
|
53
|
-
return { percentage, contextLeftPercentage: Math.max(0, 100 - percentage) };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Calculate context info from native Claude Code context_window data (preferred).
|
|
58
|
-
* Requires Claude Code 2.0.70+ with current_usage field.
|
|
59
|
-
*/
|
|
60
|
-
calculateContextFromHookData(hookData: ClaudeHookData): ContextInfo | null {
|
|
61
|
-
const cw = hookData.context_window;
|
|
62
|
-
if (!cw?.current_usage) {
|
|
63
|
-
debug(
|
|
64
|
-
"No current_usage in hook data, falling back to transcript parsing",
|
|
65
|
-
);
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const currentUsage = cw.current_usage;
|
|
70
|
-
// [LAW:no-defensive-null-guards] context_window_size is a required `number`
|
|
71
|
-
// within context_window; reaching here proves cw is present, so the size
|
|
72
|
-
// is too. No `|| default` — that would mask a malformed payload as 200k.
|
|
73
|
-
const contextLimit = cw.context_window_size;
|
|
74
|
-
const totalTokens =
|
|
75
|
-
(currentUsage.input_tokens || 0) +
|
|
76
|
-
(currentUsage.cache_creation_input_tokens || 0) +
|
|
77
|
-
(currentUsage.cache_read_input_tokens || 0);
|
|
78
|
-
|
|
79
|
-
debug(
|
|
80
|
-
`Native current_usage: input=${currentUsage.input_tokens}, cache_create=${currentUsage.cache_creation_input_tokens}, cache_read=${currentUsage.cache_read_input_tokens}, total=${totalTokens} (limit: ${contextLimit})`,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// [LAW:one-source-of-truth] Claude's reported used/remaining percentages
|
|
84
|
-
// are authoritative; the token-ratio is only a floor for the window whose
|
|
85
|
-
// percentages are still null (pre-first-call). remaining_percentage is NOT
|
|
86
|
-
// recomputed from a local buffer — it measures real headroom to the limit.
|
|
87
|
-
const ratio = this.ratioPercentages(totalTokens, contextLimit);
|
|
88
|
-
return {
|
|
89
|
-
totalTokens,
|
|
90
|
-
maxTokens: contextLimit,
|
|
91
|
-
percentage:
|
|
92
|
-
cw.used_percentage != null
|
|
93
|
-
? Math.round(cw.used_percentage)
|
|
94
|
-
: ratio.percentage,
|
|
95
|
-
contextLeftPercentage:
|
|
96
|
-
cw.remaining_percentage != null
|
|
97
|
-
? Math.round(cw.remaining_percentage)
|
|
98
|
-
: ratio.contextLeftPercentage,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Calculate context tokens by parsing the transcript file (fallback).
|
|
104
|
-
* Used for older Claude Code versions that don't provide context_window.
|
|
105
|
-
*
|
|
106
|
-
* [LAW:no-silent-failure] An unreadable transcript is `failed` (the payload
|
|
107
|
-
* boundary logs it); a transcript with no usable usage entry is `absent`.
|
|
108
|
-
* The old catch-to-null collapsed both into "no data".
|
|
109
|
-
*/
|
|
110
|
-
async calculateContextTokensFromTranscript(
|
|
111
|
-
transcriptPath: string,
|
|
112
|
-
contextLimit: number,
|
|
113
|
-
): Promise<Outcome<ContextInfo>> {
|
|
114
|
-
try {
|
|
115
|
-
debug(`Calculating context tokens from transcript: ${transcriptPath}`);
|
|
116
|
-
|
|
117
|
-
const parsedEntries = await parseJsonlFile(transcriptPath);
|
|
118
|
-
|
|
119
|
-
if (parsedEntries.length === 0) {
|
|
120
|
-
debug("No entries in transcript");
|
|
121
|
-
return ABSENT;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let mostRecentEntry: ParsedEntry | null = null;
|
|
125
|
-
|
|
126
|
-
for (let i = parsedEntries.length - 1; i >= 0; i--) {
|
|
127
|
-
const entry = parsedEntries[i];
|
|
128
|
-
if (!entry) continue;
|
|
129
|
-
|
|
130
|
-
if (!entry.message?.usage?.input_tokens) continue;
|
|
131
|
-
if (entry.isSidechain === true) continue;
|
|
132
|
-
|
|
133
|
-
mostRecentEntry = entry;
|
|
134
|
-
debug(
|
|
135
|
-
`Context segment: Found most recent entry at ${entry.timestamp.toISOString()}, stopping search`,
|
|
136
|
-
);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (mostRecentEntry?.message?.usage) {
|
|
141
|
-
const usage = mostRecentEntry.message.usage;
|
|
142
|
-
const totalTokens =
|
|
143
|
-
(usage.input_tokens || 0) +
|
|
144
|
-
(usage.cache_read_input_tokens || 0) +
|
|
145
|
-
(usage.cache_creation_input_tokens || 0);
|
|
146
|
-
|
|
147
|
-
debug(
|
|
148
|
-
`Most recent main chain context: ${totalTokens} tokens (limit: ${contextLimit})`,
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
return ok({
|
|
152
|
-
totalTokens,
|
|
153
|
-
maxTokens: contextLimit,
|
|
154
|
-
...this.ratioPercentages(totalTokens, contextLimit),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
debug("No main chain entries with usage data found");
|
|
159
|
-
return ABSENT;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
return failed(
|
|
162
|
-
`context transcript: ${error instanceof Error ? error.message : String(error)}`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get context info using native data if available, falling back to transcript parsing.
|
|
169
|
-
*/
|
|
170
|
-
async getContextInfo(
|
|
171
|
-
hookData: ClaudeHookData,
|
|
172
|
-
): Promise<Outcome<ContextInfo>> {
|
|
173
|
-
const nativeContext = this.calculateContextFromHookData(hookData);
|
|
174
|
-
if (nativeContext) {
|
|
175
|
-
return ok(nativeContext);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// [LAW:one-source-of-truth] current_usage can be null (pre-first-call or
|
|
179
|
-
// post-/compact) while context_window_size is still present and
|
|
180
|
-
// authoritative — prefer it here too, and only fall to the floor when the
|
|
181
|
-
// client omits context_window entirely.
|
|
182
|
-
const contextLimit =
|
|
183
|
-
hookData.context_window?.context_window_size ?? DEFAULT_CONTEXT_WINDOW;
|
|
184
|
-
|
|
185
|
-
return this.calculateContextTokensFromTranscript(
|
|
186
|
-
hookData.transcript_path,
|
|
187
|
-
contextLimit,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
}
|