clementine-agent 1.18.181 → 1.18.183

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.
@@ -219,18 +219,26 @@ function shouldAutoDelegate(skill, source) {
219
219
  return true;
220
220
  }
221
221
  /**
222
- * Resolve the model string to use for an autonomous run. The 1M-context
223
- * variant gives the worker subagent 5× the room of the standard 200K
224
- * window — enough headroom that compaction is rare and the
225
- * "refetch-after-compact" loop pattern (seen in the 2026-05-11
226
- * imessage-triage failures) never occurs in practice.
222
+ * Resolve the model string to use for an autonomous run.
227
223
  *
228
- * The actual 1M routing is gated by the user's plan (see
229
- * config.ts:usesOneMillionContext) and the model family Haiku doesn't
230
- * support 1M, and Sonnet 1M needs the [1m] suffix. We return the full
231
- * Sonnet model ID with [1m] appended; downstream
232
- * normalizeClaudeSdkOptionsForOneMillionContext strips it back off when
233
- * the plan doesn't support it.
224
+ * **Default: plain Sonnet (200K).** Sonnet `[1m]` is the "Extra Usage
225
+ * path" on Anthropic's billingit is NOT covered by Max/Team/Enterprise
226
+ * subscriptions, regardless of `CLEMENTINE_1M_CONTEXT_MODE` (the mode
227
+ * flag only governs Opus long-context, which Max does cover). Defaulting
228
+ * autonomous work to Sonnet [1m] silently routes cron, scheduled-skill,
229
+ * heartbeat, and team-task runs onto a separate metered bill — surprising
230
+ * on Max plans where the standard Sonnet meter stays quiet but weekly
231
+ * usage climbs.
232
+ *
233
+ * Compaction risk on the 200K window is mitigated by the auto-delegating
234
+ * wrapper (1.18.173): the worker subagent runs in an isolated context
235
+ * containing only the skill body + its own tool turns, so even
236
+ * data-heavy procedures comfortably fit.
237
+ *
238
+ * Skills that genuinely need 1M (rare — verify the workload first) opt
239
+ * in explicitly via frontmatter `clementine.limits.model:
240
+ * claude-sonnet-4-6[1m]` (Extra Usage) or `claude-opus-4-7[1m]` (covered
241
+ * by Max). Callers may also override per-invocation via `options.model`.
234
242
  */
235
243
  function resolveAutonomousModel(explicitModel, skillModel) {
236
244
  // Caller's explicit model wins.
@@ -239,22 +247,17 @@ function resolveAutonomousModel(explicitModel, skillModel) {
239
247
  // Skill-declared model wins next.
240
248
  if (skillModel)
241
249
  return skillModel;
242
- // Default: Sonnet [1m]. The normalizer will strip [1m] if the user's
243
- // plan doesn't include it, falling back to standard Sonnet — still
244
- // works, just with less headroom.
245
- const base = MODELS.sonnet;
246
- if (!base)
247
- return undefined;
248
- if (/\[1m\]/i.test(base))
249
- return base;
250
- return `${base}[1m]`;
250
+ // Default: plain Sonnet (no [1m]). Stays on the standard Sonnet meter
251
+ // covered by Max plans; no Extra Usage exposure.
252
+ return MODELS.sonnet;
251
253
  }
252
254
  /**
253
255
  * Build the AgentDefinition for the `skill-worker` subagent that
254
256
  * executes this skill in an isolated context. The subagent's system
255
257
  * prompt is the skill body; its tools are the skill's computed
256
- * allowlist; its model is the same 1M-context model the parent uses
257
- * (the worker is where the real data flows — the parent stays tiny).
258
+ * allowlist; its model is whatever resolveAutonomousModel returned
259
+ * by default plain Sonnet 200K, which the isolated worker context
260
+ * comfortably fits without compaction.
258
261
  *
259
262
  * `description` is what the SDK shows the parent for routing decisions.
260
263
  * Since the parent is `forceSubagent`'d to this worker, the description
@@ -275,8 +278,9 @@ function buildSkillWorkerAgent(skill, renderedProcedure, effectiveTools, model,
275
278
  `## Procedure\n\n${renderedProcedure}`,
276
279
  tools: effectiveTools,
277
280
  // SDK accepts 'sonnet' / 'opus' / 'haiku' tier aliases OR full model
278
- // IDs. We pass the full ID with [1m] when present; the SDK strips
279
- // [1m] internally for plans that don't support it.
281
+ // IDs. Default is plain Sonnet (200K); when a skill or caller opts
282
+ // into a [1m] variant explicitly, we pass it through and the SDK
283
+ // strips [1m] internally for plans that don't support it.
280
284
  ...(model ? { model } : {}),
281
285
  effort: 'medium',
282
286
  maxTurns: workerMaxTurns,
@@ -368,10 +372,12 @@ export async function runSkill(name, options = {}) {
368
372
  ...(skill.layout === 'folder' ? [path.dirname(skill.filePath)] : []),
369
373
  ];
370
374
  const mutatingSkill = effectiveTools.some((t) => t === 'Write' || t === 'Edit' || t === 'Bash' || /__(write|edit|update|create|delete|send|post|patch|set)/i.test(t));
371
- // 1.18.173: resolve the effective model. Autonomous runs default to
372
- // Sonnet [1m] (1M context window) so the worker subagent has the
373
- // room of a standard 200K-window model. resolveAutonomousModel honors
374
- // explicit overrides + skill-declared limits.model first.
375
+ // 1.18.182: resolve the effective model. Autonomous runs default to
376
+ // plain Sonnet (200K) covered by the standard Sonnet meter on Max,
377
+ // no Extra Usage exposure. Worker-subagent isolation (1.18.173) keeps
378
+ // the 200K window comfortably under compaction even for heavy skills.
379
+ // resolveAutonomousModel honors explicit overrides + skill-declared
380
+ // limits.model first, so a skill that genuinely needs 1M can opt in.
375
381
  const skillModel = skill.frontmatter?.clementine?.limits?.model;
376
382
  const effectiveModel = autoDelegate
377
383
  ? resolveAutonomousModel(options.model, skillModel)
@@ -10667,7 +10667,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10667
10667
  // ── Token Usage API ──────────────────────────────────────────────
10668
10668
  app.get('/api/metrics/usage', async (_req, res) => {
10669
10669
  if (!existsSync(MEMORY_DB_PATH)) {
10670
- res.json({ error: 'No DB', totalTokens: 0, byModel: [], bySource: [], byDay: [] });
10670
+ res.json({ error: 'No DB', totalTokens: 0, byModel: [], bySource: [], byDay: [], byBucket: [], bucketTotals: { planCostCents: 0, extraCostCents: 0 } });
10671
10671
  return;
10672
10672
  }
10673
10673
  const Database = (await import('better-sqlite3')).default;
@@ -10676,32 +10676,86 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10676
10676
  // Check if table exists
10677
10677
  const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='usage_log'").get();
10678
10678
  if (!tableExists) {
10679
- res.json({ totalTokens: 0, totalInput: 0, totalOutput: 0, byModel: [], bySource: [], byDay: [] });
10679
+ res.json({ totalTokens: 0, totalInput: 0, totalOutput: 0, byModel: [], bySource: [], byDay: [], byBucket: [], bucketTotals: { planCostCents: 0, extraCostCents: 0 } });
10680
10680
  return;
10681
10681
  }
10682
+ // 1.18.183: cost_cents may not exist on older installs (added via
10683
+ // ALTER at store.ts:675). Probe before referencing so we degrade
10684
+ // to "no cost data" rather than erroring out.
10685
+ const columns = new Set(db.prepare('PRAGMA table_info(usage_log)').all().map((c) => c.name));
10686
+ const costExpr = columns.has('cost_cents') ? 'COALESCE(SUM(cost_cents), 0)' : '0';
10682
10687
  const totals = db.prepare(`SELECT COALESCE(SUM(input_tokens), 0) as ti, COALESCE(SUM(output_tokens), 0) as to_,
10683
- COALESCE(SUM(cache_read_tokens), 0) as tcr, COALESCE(SUM(cache_creation_tokens), 0) as tcc
10688
+ COALESCE(SUM(cache_read_tokens), 0) as tcr, COALESCE(SUM(cache_creation_tokens), 0) as tcc,
10689
+ ${costExpr} as cost
10684
10690
  FROM usage_log`).get();
10685
- const byModel = db.prepare(`SELECT model, SUM(input_tokens) as input, SUM(output_tokens) as output, SUM(cache_read_tokens) as cacheRead
10691
+ const byModel = db.prepare(`SELECT model, SUM(input_tokens) as input, SUM(output_tokens) as output,
10692
+ SUM(cache_read_tokens) as cacheRead, ${costExpr} as costCents, COUNT(*) as queries
10686
10693
  FROM usage_log GROUP BY model ORDER BY input DESC`).all();
10687
10694
  const bySource = db.prepare(`SELECT source, SUM(input_tokens) as input, SUM(output_tokens) as output
10688
10695
  FROM usage_log GROUP BY source ORDER BY input DESC`).all();
10689
10696
  const byDay = db.prepare(`SELECT date(created_at) as day, SUM(input_tokens) as input, SUM(output_tokens) as output
10690
10697
  FROM usage_log WHERE created_at >= date('now', '-7 days')
10691
10698
  GROUP BY date(created_at) ORDER BY day`).all();
10699
+ // 1.18.183: per-bucket aggregation. Same byModel rows, classified
10700
+ // by billing bucket (Sonnet 200K / Sonnet 1M Extra Usage / Opus /
10701
+ // Opus 1M / Haiku) so the dashboard can render Max-meter vs
10702
+ // Extra-Usage breakdown. See src/lib/billing-buckets.ts.
10703
+ const { classifyBillingBucket, BUCKET_DISPLAY_ORDER } = await import('../lib/billing-buckets.js');
10704
+ const bucketMap = new Map();
10705
+ for (const row of byModel) {
10706
+ const b = classifyBillingBucket(row.model);
10707
+ const existing = bucketMap.get(b.id);
10708
+ if (existing) {
10709
+ existing.costCents += Number(row.costCents) || 0;
10710
+ existing.inputTokens += Number(row.input) || 0;
10711
+ existing.outputTokens += Number(row.output) || 0;
10712
+ existing.queries += Number(row.queries) || 0;
10713
+ if (!existing.models.includes(row.model))
10714
+ existing.models.push(row.model);
10715
+ }
10716
+ else {
10717
+ bucketMap.set(b.id, {
10718
+ id: b.id,
10719
+ label: b.label,
10720
+ family: b.family,
10721
+ context: b.context,
10722
+ meteredOnMax: b.meteredOnMax,
10723
+ costCents: Number(row.costCents) || 0,
10724
+ inputTokens: Number(row.input) || 0,
10725
+ outputTokens: Number(row.output) || 0,
10726
+ queries: Number(row.queries) || 0,
10727
+ models: [row.model],
10728
+ });
10729
+ }
10730
+ }
10731
+ // Render in canonical order (Extra Usage anchors last so callouts
10732
+ // hit the eye after the in-plan rows).
10733
+ const byBucket = BUCKET_DISPLAY_ORDER
10734
+ .map((id) => bucketMap.get(id))
10735
+ .filter((b) => b !== undefined);
10736
+ const bucketTotals = byBucket.reduce((acc, b) => {
10737
+ if (b.meteredOnMax === 'extra')
10738
+ acc.extraCostCents += b.costCents;
10739
+ else
10740
+ acc.planCostCents += b.costCents;
10741
+ return acc;
10742
+ }, { planCostCents: 0, extraCostCents: 0 });
10692
10743
  res.json({
10693
10744
  totalInput: totals.ti,
10694
10745
  totalOutput: totals.to_,
10695
10746
  totalCacheRead: totals.tcr,
10696
10747
  totalCacheCreation: totals.tcc,
10697
10748
  totalTokens: totals.ti + totals.to_,
10749
+ totalCostCents: totals.cost,
10698
10750
  byModel,
10699
10751
  bySource,
10700
10752
  byDay,
10753
+ byBucket,
10754
+ bucketTotals,
10701
10755
  });
10702
10756
  }
10703
10757
  catch (err) {
10704
- res.json({ error: String(err), totalTokens: 0, byModel: [], bySource: [], byDay: [] });
10758
+ res.json({ error: String(err), totalTokens: 0, byModel: [], bySource: [], byDay: [], byBucket: [], bucketTotals: { planCostCents: 0, extraCostCents: 0 } });
10705
10759
  }
10706
10760
  finally {
10707
10761
  db.close();
@@ -37199,6 +37253,16 @@ function formatTokens(n) {
37199
37253
  return String(n);
37200
37254
  }
37201
37255
 
37256
+ // 1.18.183: dollars for the billing-bucket panel. Cents in, "$X.XX"
37257
+ // out. Sub-cent values render as "<$0.01" so a tiny but nonzero
37258
+ // Extra Usage line still reads as "you have exposure" rather than "$0".
37259
+ function formatCents(cents) {
37260
+ var c = Number(cents) || 0;
37261
+ if (c === 0) return '$0.00';
37262
+ if (c > 0 && c < 1) return '<$0.01';
37263
+ return '$' + (c / 100).toFixed(2);
37264
+ }
37265
+
37202
37266
  function formatBytes(n) {
37203
37267
  if (n == null) return '—';
37204
37268
  if (n < 1024) return n + ' B';
@@ -38610,6 +38674,65 @@ async function refreshMetrics() {
38610
38674
  html += statTile(cacheEff + '%', 'Cache Hit Rate', cacheEff >= 50 ? 'var(--green)' : cacheEff >= 20 ? 'var(--yellow)' : 'var(--text-muted)');
38611
38675
  html += '</div>';
38612
38676
 
38677
+ // 1.18.183: Spend by Billing Bucket — separates Max-covered usage
38678
+ // from Extra Usage exposure (Sonnet [1m] etc.). Surfaces the bucket
38679
+ // a model lives in, not just its token count, so a quiet Sonnet
38680
+ // meter no longer hides rising weekly spend on a separate billing
38681
+ // line. See src/lib/billing-buckets.ts.
38682
+ if (u.byBucket && u.byBucket.length > 0) {
38683
+ var planCost = (u.bucketTotals && u.bucketTotals.planCostCents) || 0;
38684
+ var extraCost = (u.bucketTotals && u.bucketTotals.extraCostCents) || 0;
38685
+ var totalCost = planCost + extraCost;
38686
+ var hasExtra = extraCost > 0;
38687
+
38688
+ html += '<div class="card" style="margin-top:16px"><div class="card-header">'
38689
+ + 'Spend by Billing Bucket'
38690
+ + '<span style="float:right;font-size:11px;color:var(--text-muted);font-weight:400">'
38691
+ + 'In-plan ' + esc(formatCents(planCost))
38692
+ + ' &middot; Extra Usage ' + (hasExtra
38693
+ ? '<span style="color:var(--orange,#f80);font-weight:600">' + esc(formatCents(extraCost)) + '</span>'
38694
+ : esc(formatCents(extraCost)))
38695
+ + '</span></div><div class="card-body">';
38696
+
38697
+ // Banner when Extra Usage > 0 — this is the whole point of the
38698
+ // panel. Max plans don't comp Sonnet [1m]; surfacing it here means
38699
+ // the user can spot it without checking the Anthropic Console.
38700
+ if (hasExtra) {
38701
+ html += '<div style="margin-bottom:12px;padding:10px 12px;border-radius:6px;'
38702
+ + 'background:rgba(255,128,0,0.08);border-left:3px solid var(--orange,#f80);'
38703
+ + 'font-size:12px;line-height:1.5">'
38704
+ + '<strong>Extra Usage detected.</strong> '
38705
+ + esc(formatCents(extraCost))
38706
+ + ' of recent spend is on the Anthropic Extra Usage path (typically Sonnet 1M), '
38707
+ + 'which is <strong>not</strong> covered by Max / Team / Enterprise subscriptions. '
38708
+ + 'Switch heavy autonomous skills to <code>claude-opus-4-7[1m]</code> via skill frontmatter '
38709
+ + '<code>clementine.limits.model</code>, or drop <code>[1m]</code> to stay on the standard Sonnet meter.'
38710
+ + '</div>';
38711
+ }
38712
+
38713
+ var maxBucketCost = Math.max.apply(null, u.byBucket.map(function(b) { return b.costCents || 0; }).concat([1]));
38714
+ for (var bi = 0; bi < u.byBucket.length; bi++) {
38715
+ var bk = u.byBucket[bi];
38716
+ var bkPct = maxBucketCost > 0 ? Math.round(((bk.costCents || 0) / maxBucketCost) * 100) : 0;
38717
+ var bkColor = bk.meteredOnMax === 'extra' ? 'var(--orange,#f80)'
38718
+ : (bk.family === 'opus' ? 'var(--purple)'
38719
+ : (bk.family === 'sonnet' ? 'var(--blue)'
38720
+ : (bk.family === 'haiku' ? 'var(--green)' : 'var(--text-muted)')));
38721
+ var bkShareNum = totalCost > 0 ? Math.round(((bk.costCents || 0) / totalCost) * 100) : 0;
38722
+ html += '<div style="margin-bottom:10px">'
38723
+ + '<div class="kv-row">'
38724
+ + '<span class="kv-key">' + esc(bk.label) + '</span>'
38725
+ + '<span class="kv-val" title="' + esc(formatTokens(bk.inputTokens || 0)) + ' input &middot; ' + esc(formatTokens(bk.outputTokens || 0)) + ' output &middot; ' + (bk.queries || 0) + ' calls">'
38726
+ + esc(formatCents(bk.costCents || 0))
38727
+ + '<span style="color:var(--text-muted);font-size:11px;margin-left:6px">' + bkShareNum + '%</span>'
38728
+ + '</span>'
38729
+ + '</div>'
38730
+ + '<div class="metric-bar-track"><div class="metric-bar-fill" style="width:' + bkPct + '%;background:' + bkColor + '"></div></div>'
38731
+ + '</div>';
38732
+ }
38733
+ html += '</div></div>';
38734
+ }
38735
+
38613
38736
  // Tokens by Model
38614
38737
  if (u.byModel && u.byModel.length > 0) {
38615
38738
  html += '<div class="card"><div class="card-header">Tokens by Model</div><div class="card-body">';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Anthropic billing-bucket classifier.
3
+ *
4
+ * Maps a Claude model string (full ID or SDK tier alias) to the metering
5
+ * bucket Anthropic bills against. The headline distinction on Max /
6
+ * Team / Enterprise plans is in-plan (covered by the subscription's
7
+ * usage allowance) vs. Extra Usage (billed separately, surprises the
8
+ * meter watcher).
9
+ *
10
+ * **Why this matters (2026-05-11)**: Sonnet `[1m]` is on the Extra Usage
11
+ * path even with Max. Max covers Opus long-context but not Sonnet 1M.
12
+ * Without per-bucket aggregation, the dashboard cost number conflates
13
+ * "covered by my plan" with "billed separately" and the user has no way
14
+ * to spot Extra Usage exposure until the invoice arrives. See
15
+ * memory/feedback_sonnet_1m_extra_usage.md.
16
+ *
17
+ * Pure function, no I/O. Safe to call from any layer.
18
+ */
19
+ export type BillingBucketId = 'sonnet' | 'sonnet-1m' | 'opus' | 'opus-1m' | 'haiku' | 'other';
20
+ export type BillingBucketMetering =
21
+ /** Counts against the Max/Team/Enterprise plan's usage allowance. */
22
+ 'plan'
23
+ /** Billed separately as Extra Usage even when the user has Max. */
24
+ | 'extra';
25
+ export interface BillingBucket {
26
+ /** Stable bucket id, suitable for grouping/keys. */
27
+ id: BillingBucketId;
28
+ /** Human-readable label for UI ("Sonnet 200K", "Sonnet 1M — Extra Usage"). */
29
+ label: string;
30
+ /** Model family irrespective of context window. */
31
+ family: 'sonnet' | 'opus' | 'haiku' | 'other';
32
+ /** Context window class. */
33
+ context: '200k' | '1m';
34
+ /** How Anthropic bills this on a Max plan. */
35
+ meteredOnMax: BillingBucketMetering;
36
+ }
37
+ /**
38
+ * Classify a model string into its billing bucket.
39
+ *
40
+ * Accepts:
41
+ * - Full model IDs: `claude-sonnet-4-6`, `claude-sonnet-4-6[1m]`,
42
+ * `claude-opus-4-7[1m]`, `claude-haiku-4-5-20251001`, etc.
43
+ * - SDK tier aliases: `sonnet`, `opus`, `haiku` (no `[1m]` form for
44
+ * tier aliases — they always resolve to standard context).
45
+ * - Empty / unknown / non-Claude strings → `'other'` bucket.
46
+ */
47
+ export declare function classifyBillingBucket(model: string | undefined | null): BillingBucket;
48
+ /** Canonical render order for the dashboard panel. */
49
+ export declare const BUCKET_DISPLAY_ORDER: readonly BillingBucketId[];
50
+ /** Convenience: is this bucket on the Extra Usage path for Max plans? */
51
+ export declare function isExtraUsage(bucket: BillingBucket): boolean;
52
+ //# sourceMappingURL=billing-buckets.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Anthropic billing-bucket classifier.
3
+ *
4
+ * Maps a Claude model string (full ID or SDK tier alias) to the metering
5
+ * bucket Anthropic bills against. The headline distinction on Max /
6
+ * Team / Enterprise plans is in-plan (covered by the subscription's
7
+ * usage allowance) vs. Extra Usage (billed separately, surprises the
8
+ * meter watcher).
9
+ *
10
+ * **Why this matters (2026-05-11)**: Sonnet `[1m]` is on the Extra Usage
11
+ * path even with Max. Max covers Opus long-context but not Sonnet 1M.
12
+ * Without per-bucket aggregation, the dashboard cost number conflates
13
+ * "covered by my plan" with "billed separately" and the user has no way
14
+ * to spot Extra Usage exposure until the invoice arrives. See
15
+ * memory/feedback_sonnet_1m_extra_usage.md.
16
+ *
17
+ * Pure function, no I/O. Safe to call from any layer.
18
+ */
19
+ /**
20
+ * Classify a model string into its billing bucket.
21
+ *
22
+ * Accepts:
23
+ * - Full model IDs: `claude-sonnet-4-6`, `claude-sonnet-4-6[1m]`,
24
+ * `claude-opus-4-7[1m]`, `claude-haiku-4-5-20251001`, etc.
25
+ * - SDK tier aliases: `sonnet`, `opus`, `haiku` (no `[1m]` form for
26
+ * tier aliases — they always resolve to standard context).
27
+ * - Empty / unknown / non-Claude strings → `'other'` bucket.
28
+ */
29
+ export function classifyBillingBucket(model) {
30
+ const m = String(model ?? '').toLowerCase().trim();
31
+ if (!m)
32
+ return OTHER;
33
+ const is1m = /\[1m\]/i.test(m);
34
+ // Tier aliases — no context-window suffix possible.
35
+ if (m === 'sonnet')
36
+ return SONNET_200K;
37
+ if (m === 'opus')
38
+ return OPUS_200K;
39
+ if (m === 'haiku')
40
+ return HAIKU;
41
+ // Full model IDs. Order matters — check opus before sonnet because
42
+ // "opusplan" contains "opus" but not "sonnet"; reverse would still be
43
+ // safe today, but explicit ordering is more robust to future names.
44
+ if (m.includes('opus'))
45
+ return is1m ? OPUS_1M : OPUS_200K;
46
+ if (m.includes('sonnet'))
47
+ return is1m ? SONNET_1M : SONNET_200K;
48
+ if (m.includes('haiku'))
49
+ return HAIKU; // 1M not supported on Haiku
50
+ return { ...OTHER, label: model || 'Unknown' };
51
+ }
52
+ /** Stable singletons so equality checks and bucket-key lookups are cheap. */
53
+ const SONNET_200K = {
54
+ id: 'sonnet',
55
+ label: 'Sonnet (200K)',
56
+ family: 'sonnet',
57
+ context: '200k',
58
+ meteredOnMax: 'plan',
59
+ };
60
+ const SONNET_1M = {
61
+ id: 'sonnet-1m',
62
+ label: 'Sonnet (1M) — Extra Usage',
63
+ family: 'sonnet',
64
+ context: '1m',
65
+ meteredOnMax: 'extra',
66
+ };
67
+ const OPUS_200K = {
68
+ id: 'opus',
69
+ label: 'Opus (200K)',
70
+ family: 'opus',
71
+ context: '200k',
72
+ meteredOnMax: 'plan',
73
+ };
74
+ const OPUS_1M = {
75
+ id: 'opus-1m',
76
+ label: 'Opus (1M)',
77
+ family: 'opus',
78
+ context: '1m',
79
+ meteredOnMax: 'plan',
80
+ };
81
+ const HAIKU = {
82
+ id: 'haiku',
83
+ label: 'Haiku',
84
+ family: 'haiku',
85
+ context: '200k',
86
+ meteredOnMax: 'plan',
87
+ };
88
+ const OTHER = {
89
+ id: 'other',
90
+ label: 'Unknown',
91
+ family: 'other',
92
+ context: '200k',
93
+ meteredOnMax: 'plan',
94
+ };
95
+ /** Canonical render order for the dashboard panel. */
96
+ export const BUCKET_DISPLAY_ORDER = [
97
+ 'sonnet',
98
+ 'haiku',
99
+ 'opus',
100
+ 'opus-1m',
101
+ 'sonnet-1m', // Extra Usage stays last so it visually anchors the callout
102
+ 'other',
103
+ ];
104
+ /** Convenience: is this bucket on the Extra Usage path for Max plans? */
105
+ export function isExtraUsage(bucket) {
106
+ return bucket.meteredOnMax === 'extra';
107
+ }
108
+ //# sourceMappingURL=billing-buckets.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.181",
3
+ "version": "1.18.183",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",