cc-usage-bar 0.1.0 → 0.4.1

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.
@@ -1,42 +1,81 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_FORMAT = exports.FORMAT_PRESETS = void 0;
3
+ exports.DEFAULT_FORMAT = exports.DEFAULT_RAMP = exports.COLOR_NAMES = exports.COLOR_MAP = exports.FORMAT_PRESETS = void 0;
4
4
  exports.formatExpiry = formatExpiry;
5
+ exports.formatCountdown = formatCountdown;
5
6
  exports.parseBarSpec = parseBarSpec;
7
+ exports.hexToAnsi = hexToAnsi;
8
+ exports.parseColorRamp = parseColorRamp;
9
+ exports.colorFromRamp = colorFromRamp;
6
10
  exports.formatBar = formatBar;
7
11
  exports.renderSubscription = renderSubscription;
8
12
  exports.renderBalance = renderBalance;
9
13
  exports.renderUsage = renderUsage;
10
14
  exports.isValidPreset = isValidPreset;
11
15
  exports.shouldUseColor = shouldUseColor;
12
- exports.FORMAT_PRESETS = ['compact', 'numeric', 'time', 'bar', 'bar-time'];
16
+ exports.FORMAT_PRESETS = [
17
+ 'compact',
18
+ 'numeric',
19
+ 'time',
20
+ 'countdown',
21
+ 'bar',
22
+ 'bar-time',
23
+ 'bar-countdown',
24
+ ];
25
+ const ANSI_RESET = '\x1b[0m';
26
+ const ANSI_REVERSE = '\x1b[7m';
27
+ exports.COLOR_MAP = {
28
+ none: '',
29
+ dim: '\x1b[2m',
30
+ black: '\x1b[30m',
31
+ red: '\x1b[31m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ blue: '\x1b[34m',
35
+ magenta: '\x1b[35m',
36
+ cyan: '\x1b[36m',
37
+ white: '\x1b[37m',
38
+ gray: '\x1b[90m',
39
+ boldRed: '\x1b[1;31m',
40
+ boldGreen: '\x1b[1;32m',
41
+ boldYellow: '\x1b[1;33m',
42
+ boldBlue: '\x1b[1;34m',
43
+ boldMagenta: '\x1b[1;35m',
44
+ boldCyan: '\x1b[1;36m',
45
+ boldWhite: '\x1b[1;37m',
46
+ };
47
+ exports.COLOR_NAMES = Object.keys(exports.COLOR_MAP);
48
+ exports.DEFAULT_RAMP = [
49
+ { min: 0, ansi: exports.COLOR_MAP.green },
50
+ { min: 60, ansi: exports.COLOR_MAP.yellow },
51
+ { min: 85, ansi: exports.COLOR_MAP.red },
52
+ ];
13
53
  exports.DEFAULT_FORMAT = {
14
- format: 'compact',
54
+ format: 'bar-countdown',
15
55
  barWidth: 10,
16
56
  color: true,
17
57
  showProviderName: true,
58
+ colorRamp5h: exports.DEFAULT_RAMP,
59
+ colorRampWk: exports.DEFAULT_RAMP,
60
+ colorRampBalance: exports.DEFAULT_RAMP,
18
61
  };
19
62
  const SUB_PRESET_TPL = {
20
63
  compact: '{label} {percent}%',
21
64
  numeric: '{percent}%',
22
65
  time: '{percent}% until {expiry}',
66
+ countdown: '{percent}% in {countdown}',
23
67
  bar: '[{bar}] {percent}%',
24
68
  'bar-time': '[{bar}] {percent}% until {expiry}',
69
+ 'bar-countdown': '[{bar}] {percent}% in {countdown}',
25
70
  };
26
71
  const BAL_PRESET_TPL = {
27
72
  compact: '{amount}',
28
73
  numeric: '{amount}',
29
74
  time: '{amount}',
75
+ countdown: '{amount}',
30
76
  bar: '[{bar}] {amount}',
31
77
  'bar-time': '[{bar}] {amount}',
32
- };
33
- const ANSI = {
34
- reset: '\x1b[0m',
35
- dim: '\x1b[2m',
36
- green: '\x1b[32m',
37
- yellow: '\x1b[33m',
38
- red: '\x1b[31m',
39
- boldRed: '\x1b[1;31m',
78
+ 'bar-countdown': '[{bar}] {amount}',
40
79
  };
41
80
  const pad2 = (n) => n.toString().padStart(2, '0');
42
81
  function formatExpiry(iso, now = new Date()) {
@@ -57,6 +96,26 @@ function formatExpiry(iso, now = new Date()) {
57
96
  }
58
97
  return `${target.getFullYear()}-${pad2(target.getMonth() + 1)}-${pad2(target.getDate())}`;
59
98
  }
99
+ function formatCountdown(iso, now = new Date()) {
100
+ if (!iso)
101
+ return '?';
102
+ const target = new Date(iso);
103
+ if (Number.isNaN(target.getTime()))
104
+ return '?';
105
+ const secs = Math.max(0, Math.floor((target.getTime() - now.getTime()) / 1000));
106
+ if (secs < 60)
107
+ return `${secs}s`;
108
+ const mins = Math.floor(secs / 60);
109
+ if (mins < 60)
110
+ return `${mins}m`;
111
+ const hrs = Math.floor(mins / 60);
112
+ const remMin = mins % 60;
113
+ if (hrs < 24)
114
+ return remMin > 0 ? `${hrs}h${remMin}m` : `${hrs}h`;
115
+ const days = Math.floor(hrs / 24);
116
+ const remHr = hrs % 24;
117
+ return remHr > 0 ? `${days}d${remHr}h` : `${days}d`;
118
+ }
60
119
  function isNonEmptyString(v) {
61
120
  return typeof v === 'string' && v.length > 0;
62
121
  }
@@ -88,6 +147,8 @@ function parseBarSpec(raw) {
88
147
  const out = { mode: 'tint', text: obj.text };
89
148
  if (obj.emptyStyle === 'plain' || obj.emptyStyle === 'dim')
90
149
  out.emptyStyle = obj.emptyStyle;
150
+ if (obj.style === 'fg' || obj.style === 'reverse')
151
+ out.style = obj.style;
91
152
  return out;
92
153
  }
93
154
  if (obj.mode === 'frames') {
@@ -100,6 +161,71 @@ function parseBarSpec(raw) {
100
161
  }
101
162
  return null;
102
163
  }
164
+ function hexToAnsi(hex) {
165
+ const m = hex.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
166
+ if (!m)
167
+ return null;
168
+ const h = m[1];
169
+ let r;
170
+ let g;
171
+ let b;
172
+ if (h.length === 3) {
173
+ r = parseInt(h[0] + h[0], 16);
174
+ g = parseInt(h[1] + h[1], 16);
175
+ b = parseInt(h[2] + h[2], 16);
176
+ }
177
+ else {
178
+ r = parseInt(h.slice(0, 2), 16);
179
+ g = parseInt(h.slice(2, 4), 16);
180
+ b = parseInt(h.slice(4, 6), 16);
181
+ }
182
+ return `\x1b[38;2;${r};${g};${b}m`;
183
+ }
184
+ function parseColorRamp(spec) {
185
+ if (!spec)
186
+ return null;
187
+ const parts = spec.split(',').map((s) => s.trim()).filter(Boolean);
188
+ if (parts.length === 0)
189
+ return null;
190
+ const rules = [];
191
+ for (const p of parts) {
192
+ const m = p.match(/^(\d+(?:\.\d+)?):(\S+)$/);
193
+ if (!m)
194
+ return null;
195
+ const min = parseFloat(m[1]);
196
+ const token = m[2];
197
+ let ansi;
198
+ if (token === 'none') {
199
+ ansi = null;
200
+ }
201
+ else if (token.startsWith('#')) {
202
+ ansi = hexToAnsi(token);
203
+ if (ansi === null)
204
+ return null;
205
+ }
206
+ else if (token in exports.COLOR_MAP) {
207
+ ansi = exports.COLOR_MAP[token] || null;
208
+ }
209
+ else {
210
+ return null;
211
+ }
212
+ rules.push({ min, ansi });
213
+ }
214
+ rules.sort((a, b) => a.min - b.min);
215
+ return rules;
216
+ }
217
+ function colorFromRamp(util, ramp) {
218
+ if (!ramp || ramp.length === 0)
219
+ return null;
220
+ let picked = null;
221
+ for (const r of ramp) {
222
+ if (util >= r.min)
223
+ picked = r.ansi;
224
+ else
225
+ break;
226
+ }
227
+ return picked;
228
+ }
103
229
  function splitText(s) {
104
230
  return Array.from(s);
105
231
  }
@@ -117,8 +243,9 @@ function formatBar(pct, width = 10, spec, activeColor = null, useColor = false)
117
243
  const inactive = chars.slice(filled).join('');
118
244
  if (!useColor)
119
245
  return active + inactive;
120
- const activePart = activeColor && active ? paint(active, activeColor, true) : active;
121
- const inactivePart = inactive && spec.emptyStyle !== 'plain' ? paint(inactive, ANSI.dim, true) : inactive;
246
+ const activeStyle = activeColor && spec.style === 'reverse' ? `${activeColor}${ANSI_REVERSE}` : activeColor;
247
+ const activePart = activeStyle && active ? paint(active, activeStyle, true) : active;
248
+ const inactivePart = inactive && spec.emptyStyle !== 'plain' ? paint(inactive, exports.COLOR_MAP.dim, true) : inactive;
122
249
  return activePart + inactivePart;
123
250
  }
124
251
  if (spec?.mode === 'frames') {
@@ -128,20 +255,30 @@ function formatBar(pct, width = 10, spec, activeColor = null, useColor = false)
128
255
  const filled = Math.round((clamped / 100) * width);
129
256
  return '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled));
130
257
  }
131
- function fiveHourColor(util) {
132
- if (util < 60)
133
- return ANSI.green;
134
- if (util < 85)
135
- return ANSI.yellow;
136
- return ANSI.red;
137
- }
138
- function weekColor(util) {
139
- return util >= 80 ? ANSI.boldRed : null;
140
- }
141
258
  function paint(text, color, useColor) {
142
259
  if (!useColor || !color)
143
260
  return text;
144
- return `${color}${text}${ANSI.reset}`;
261
+ return `${color}${text}${ANSI_RESET}`;
262
+ }
263
+ function applyTierPaint(text, color, opts, tpl) {
264
+ return paint(text, color, opts.color);
265
+ }
266
+ function applyTintTemplate(tpl, vars, color, opts) {
267
+ if (!opts.color || !color)
268
+ return applyTemplate(tpl, vars);
269
+ return tpl
270
+ .split(/(\{bar\})/g)
271
+ .map((part) => (part === '{bar}' ? vars.bar ?? '{bar}' : paint(applyTemplate(part, vars), color, true)))
272
+ .join('');
273
+ }
274
+ function renderTemplate(tpl, vars, color, opts) {
275
+ if (opts.barSpec?.mode === 'tint' && tpl.includes('{bar}')) {
276
+ return applyTintTemplate(tpl, vars, color, opts);
277
+ }
278
+ return applyTierPaint(applyTemplate(tpl, vars), color, opts, tpl);
279
+ }
280
+ function prependProvider(text, planName, opts) {
281
+ return opts.showProviderName && planName ? `${planName} ${text}` : text;
145
282
  }
146
283
  function applyTemplate(tpl, vars) {
147
284
  return tpl.replace(/\{(\w+)\}/g, (_, k) => (k in vars ? vars[k] : `{${k}}`));
@@ -149,17 +286,17 @@ function applyTemplate(tpl, vars) {
149
286
  function renderTier(ctx, opts) {
150
287
  const pct = Math.round(ctx.tier.utilization);
151
288
  const tpl = opts.template ?? SUB_PRESET_TPL[opts.format];
152
- const color = ctx.label === '5h' ? fiveHourColor(pct) : weekColor(pct);
153
- const text = applyTemplate(tpl, {
289
+ const ramp = ctx.label === '5h' ? opts.colorRamp5h ?? exports.DEFAULT_RAMP : opts.colorRampWk ?? exports.DEFAULT_RAMP;
290
+ const color = colorFromRamp(pct, ramp);
291
+ const vars = {
154
292
  label: ctx.label,
155
293
  percent: String(pct),
156
294
  bar: formatBar(pct, opts.barWidth, opts.barSpec, color, opts.color),
157
295
  expiry: formatExpiry(ctx.tier.resets_at, opts.now),
296
+ countdown: formatCountdown(ctx.tier.resets_at, opts.now),
158
297
  provider: ctx.provider,
159
- });
160
- if (opts.barSpec?.mode === 'tint' && opts.color && tpl.includes('{bar}'))
161
- return text;
162
- return paint(text, color, opts.color);
298
+ };
299
+ return renderTemplate(tpl, vars, color, opts);
163
300
  }
164
301
  function currencySymbol(unit) {
165
302
  switch (unit.toUpperCase()) {
@@ -186,35 +323,35 @@ function renderSubscription(d, opts) {
186
323
  if (parts.length === 0)
187
324
  return '';
188
325
  const sep = opts.format === 'compact' && !opts.template ? ' ' : ' / ';
189
- const body = parts.join(sep);
190
- return opts.showProviderName && d.planName ? `${d.planName} ${body}` : body;
326
+ return prependProvider(parts.join(sep), d.planName, opts);
191
327
  }
192
328
  function renderBalance(d, opts) {
193
329
  if (d.isValid === false && d.invalidMessage) {
194
- const msg = paint(d.invalidMessage, ANSI.red, opts.color);
195
- return opts.showProviderName && d.planName ? `${d.planName} ${msg}` : msg;
196
- }
197
- const amount = typeof d.total === 'number'
198
- ? `${fmtMoney(d.remaining, d.unit)}/${fmtMoney(d.total, d.unit)}`
199
- : fmtMoney(d.remaining, d.unit);
200
- const usedPct = typeof d.total === 'number' && d.total > 0
201
- ? ((d.total - d.remaining) / d.total) * 100
202
- : 0;
330
+ return prependProvider(paint(d.invalidMessage, exports.COLOR_MAP.red, opts.color), d.planName, opts);
331
+ }
332
+ if (typeof d.total !== 'number') {
333
+ const tpl = opts.template ?? BAL_PRESET_TPL.compact;
334
+ const vars = {
335
+ label: '', percent: '0', bar: '', expiry: '', countdown: '',
336
+ provider: d.planName ?? '',
337
+ amount: fmtMoney(d.remaining, d.unit),
338
+ };
339
+ return prependProvider(renderTemplate(tpl, vars, null, opts), d.planName, opts);
340
+ }
341
+ const total = d.total;
342
+ const usedPct = total > 0 ? ((total - d.remaining) / total) * 100 : 0;
203
343
  const tpl = opts.template ?? BAL_PRESET_TPL[opts.format];
204
- const color = typeof d.total === 'number' ? fiveHourColor(usedPct) : null;
205
- const text = applyTemplate(tpl, {
344
+ const color = colorFromRamp(usedPct, opts.colorRampBalance ?? exports.DEFAULT_RAMP);
345
+ const vars = {
206
346
  label: '',
207
347
  percent: String(Math.round(usedPct)),
208
- bar: typeof d.total === 'number' ? formatBar(usedPct, opts.barWidth, opts.barSpec, color, opts.color) : '',
348
+ bar: formatBar(usedPct, opts.barWidth, opts.barSpec, color, opts.color),
209
349
  expiry: '',
350
+ countdown: '',
210
351
  provider: d.planName ?? '',
211
- amount,
212
- });
213
- if (opts.barSpec?.mode === 'tint' && opts.color && tpl.includes('{bar}')) {
214
- return opts.showProviderName && d.planName ? `${d.planName} ${text}` : text;
215
- }
216
- const colored = paint(text, color, opts.color);
217
- return opts.showProviderName && d.planName ? `${d.planName} ${colored}` : colored;
352
+ amount: `${fmtMoney(d.remaining, d.unit)}/${fmtMoney(total, d.unit)}`,
353
+ };
354
+ return prependProvider(renderTemplate(tpl, vars, color, opts), d.planName, opts);
218
355
  }
219
356
  function renderUsage(data, opts) {
220
357
  if (!data)
@@ -46,7 +46,7 @@ exports.anthropicAdapter = {
46
46
  if (res.authFailed)
47
47
  return { ok: false, authFailed: true, error: res.error };
48
48
  if (!res.ok || !res.body)
49
- return { ok: false, error: res.error ?? 'request failed' };
49
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
50
50
  const data = parseAnthropic(res.body);
51
51
  if (!data)
52
52
  return { ok: false, error: 'invalid response shape' };
@@ -46,7 +46,7 @@ exports.deepseekAdapter = {
46
46
  if (res.authFailed)
47
47
  return { ok: false, authFailed: true, error: res.error };
48
48
  if (!res.ok || !res.body)
49
- return { ok: false, error: res.error ?? 'request failed' };
49
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
50
50
  const data = parseDeepSeek(res.body);
51
51
  if (!data)
52
52
  return { ok: false, error: 'invalid response shape' };
@@ -48,7 +48,7 @@ exports.glmAdapter = {
48
48
  if (res.authFailed)
49
49
  return { ok: false, authFailed: true, error: res.error };
50
50
  if (!res.ok || !res.body)
51
- return { ok: false, error: res.error ?? 'request failed' };
51
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
52
52
  const parsed = parseGlm(res.body);
53
53
  if (!parsed)
54
54
  return { ok: false, error: 'invalid response shape' };
@@ -48,7 +48,7 @@ exports.kimiAdapter = {
48
48
  if (res.authFailed)
49
49
  return { ok: false, authFailed: true, error: res.error };
50
50
  if (!res.ok || !res.body)
51
- return { ok: false, error: res.error ?? 'request failed' };
51
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
52
52
  const data = parseKimi(res.body);
53
53
  if (!data)
54
54
  return { ok: false, error: 'invalid response shape' };
@@ -55,7 +55,7 @@ exports.minimaxAdapter = {
55
55
  if (res.authFailed)
56
56
  return { ok: false, authFailed: true, error: res.error };
57
57
  if (!res.ok || !res.body)
58
- return { ok: false, error: res.error ?? 'request failed' };
58
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
59
59
  const parsed = parseMinimax(res.body);
60
60
  if (!parsed)
61
61
  return { ok: false, error: 'invalid response shape' };
@@ -34,7 +34,7 @@ exports.novitaAdapter = {
34
34
  if (res.authFailed)
35
35
  return { ok: false, authFailed: true, error: res.error };
36
36
  if (!res.ok || !res.body)
37
- return { ok: false, error: res.error ?? 'request failed' };
37
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
38
38
  const data = parseNovita(res.body);
39
39
  if (!data)
40
40
  return { ok: false, error: 'invalid response shape' };
@@ -38,7 +38,7 @@ exports.openrouterAdapter = {
38
38
  if (res.authFailed)
39
39
  return { ok: false, authFailed: true, error: res.error };
40
40
  if (!res.ok || !res.body)
41
- return { ok: false, error: res.error ?? 'request failed' };
41
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
42
42
  const data = parseOpenRouter(res.body);
43
43
  if (!data)
44
44
  return { ok: false, error: 'invalid response shape' };
@@ -46,7 +46,7 @@ exports.siliconflowAdapter = {
46
46
  if (res.authFailed)
47
47
  return { ok: false, authFailed: true, error: res.error };
48
48
  if (!res.ok || !res.body)
49
- return { ok: false, error: res.error ?? 'request failed' };
49
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
50
50
  const data = parseSiliconFlow(res.body);
51
51
  if (!data)
52
52
  return { ok: false, error: 'invalid response shape' };
@@ -43,7 +43,7 @@ exports.stepfunAdapter = {
43
43
  if (res.authFailed)
44
44
  return { ok: false, authFailed: true, error: res.error };
45
45
  if (!res.ok || !res.body)
46
- return { ok: false, error: res.error ?? 'request failed' };
46
+ return { ok: false, status: res.status, error: res.error ?? 'request failed' };
47
47
  const data = parseStepFun(res.body);
48
48
  if (!data)
49
49
  return { ok: false, error: 'invalid response shape' };
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "shouldUseColor", { enumerable: true, get: functi
8
8
  function toFormatOptions(opts) {
9
9
  return {
10
10
  ...format_1.DEFAULT_FORMAT,
11
+ format: 'compact',
11
12
  color: opts.color,
12
13
  showProviderName: opts.showProviderName ?? false,
13
14
  };
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SettingsError = exports.FETCH_BIN = exports.BACKUP_PREFIX = exports.SETTINGS_PATH = exports.CLAUDE_DIR = void 0;
36
+ exports.SettingsError = exports.WRAP_BIN = exports.FETCH_BIN = exports.BACKUP_PREFIX = exports.SETTINGS_PATH = exports.CLAUDE_DIR = void 0;
37
37
  exports.claudeDirExists = claudeDirExists;
38
38
  exports.readSettings = readSettings;
39
39
  exports.writeSettings = writeSettings;
@@ -41,6 +41,7 @@ exports.backupSettings = backupSettings;
41
41
  exports.findBackups = findBackups;
42
42
  exports.findLatestBackup = findLatestBackup;
43
43
  exports.isInstalled = isInstalled;
44
+ exports.shellQuoteForPlatform = shellQuoteForPlatform;
44
45
  exports.applyInstall = applyInstall;
45
46
  exports.applyUninstallSurgical = applyUninstallSurgical;
46
47
  exports.restoreFromBackup = restoreFromBackup;
@@ -52,6 +53,7 @@ exports.CLAUDE_DIR = path.join(os.homedir(), '.claude');
52
53
  exports.SETTINGS_PATH = path.join(exports.CLAUDE_DIR, 'settings.json');
53
54
  exports.BACKUP_PREFIX = 'settings.json.backup-cc-usage-';
54
55
  exports.FETCH_BIN = 'cc-usage-fetch';
56
+ exports.WRAP_BIN = 'cc-usage-bar-wrap';
55
57
  class SettingsError extends Error {
56
58
  hint;
57
59
  constructor(message, hint) {
@@ -121,74 +123,98 @@ function unwrapPrior(existing) {
121
123
  }
122
124
  return undefined;
123
125
  }
124
- function shellQuote(s) {
126
+ function shellQuotePosix(s) {
125
127
  return `'${s.replace(/'/g, `'\\''`)}'`;
126
128
  }
127
- function buildFetchCommand(opts) {
128
- const parts = [exports.FETCH_BIN];
129
+ function shellQuoteWindows(s) {
130
+ return `"${s.replace(/"/g, '\\"')}"`;
131
+ }
132
+ function shellQuoteForPlatform(s, platform = process.platform) {
133
+ return platform === 'win32' ? shellQuoteWindows(s) : shellQuotePosix(s);
134
+ }
135
+ function buildFetchFlags(opts, platform) {
136
+ const parts = [];
129
137
  if (opts.format)
130
138
  parts.push(`--format=${opts.format}`);
131
139
  if (typeof opts.barWidth === 'number')
132
140
  parts.push(`--bar-width=${opts.barWidth}`);
133
141
  if (opts.barSpec)
134
- parts.push(`--bar-spec=${shellQuote(opts.barSpec)}`);
142
+ parts.push(`--bar-spec=${shellQuoteForPlatform(opts.barSpec, platform)}`);
135
143
  return parts.join(' ');
136
144
  }
137
- function applyInstall(s, opts = {}) {
145
+ function buildBareFetchCommand(opts, platform) {
146
+ const flags = buildFetchFlags(opts, platform);
147
+ return flags ? `${exports.FETCH_BIN} ${flags}` : exports.FETCH_BIN;
148
+ }
149
+ function buildWrapCommand(prefix, opts, platform) {
150
+ const flags = buildFetchFlags(opts, platform);
151
+ const head = `${exports.WRAP_BIN} --prefix=${shellQuoteForPlatform(prefix, platform)}`;
152
+ return flags ? `${head} ${flags}` : head;
153
+ }
154
+ function resolveBasePrefix(sl) {
155
+ if (!sl)
156
+ return undefined;
157
+ if (sl._ccUsageInstalled)
158
+ return unwrapPrior(sl);
159
+ if (sl.command && (sl.command.includes(exports.FETCH_BIN) || sl.command.includes(exports.WRAP_BIN))) {
160
+ return applyUninstallSurgical({ statusLine: sl }).statusLine;
161
+ }
162
+ return sl;
163
+ }
164
+ function applyInstall(s, opts = {}, platform = process.platform) {
138
165
  const next = { ...s };
139
- const baseExisting = unwrapPrior(next.statusLine);
140
- const fetchCmd = buildFetchCommand(opts);
166
+ const baseExisting = resolveBasePrefix(next.statusLine);
141
167
  if (!baseExisting || !baseExisting.command) {
142
168
  next.statusLine = {
143
169
  ...(baseExisting ?? {}),
144
170
  type: baseExisting?.type ?? 'command',
145
- command: fetchCmd,
171
+ command: buildBareFetchCommand(opts, platform),
146
172
  refreshInterval: baseExisting?.refreshInterval ?? 30,
147
173
  _ccUsageInstalled: true,
148
174
  };
149
175
  return next;
150
176
  }
151
177
  const original = baseExisting.command;
152
- const escaped = original.replace(/'/g, `'\\''`);
153
- const escapedFetchCmd = fetchCmd.replace(/'/g, `'\\''`);
154
- const wrapped = `sh -c 'printf "%s " "$(${escaped})"; ${escapedFetchCmd}'`;
155
178
  next.statusLine = {
156
179
  ...baseExisting,
157
- command: wrapped,
180
+ command: buildWrapCommand(original, opts, platform),
158
181
  refreshInterval: baseExisting.refreshInterval ?? 30,
159
182
  _ccUsageInstalled: true,
160
183
  _ccUsageOriginal: original,
161
184
  };
162
185
  return next;
163
186
  }
187
+ function restoreCommand(s, inner) {
188
+ const next = { ...s };
189
+ const sl = { ...s.statusLine, command: inner };
190
+ delete sl._ccUsageInstalled;
191
+ delete sl._ccUsageOriginal;
192
+ next.statusLine = sl;
193
+ return next;
194
+ }
164
195
  function applyUninstallSurgical(s) {
165
196
  if (!s.statusLine?.command)
166
197
  return s;
167
198
  const cmd = s.statusLine.command;
168
- if (!cmd.includes(exports.FETCH_BIN))
199
+ if (!cmd.includes(exports.FETCH_BIN) && !cmd.includes(exports.WRAP_BIN))
169
200
  return s;
170
- const currentWrapMatch = cmd.match(/^sh -c 'printf "%s " "\$\((.*)\)"; cc-usage-fetch.*'$/s);
171
- if (currentWrapMatch) {
172
- const inner = currentWrapMatch[1].replace(/'\\''/g, "'");
173
- const next = { ...s };
174
- const sl = { ...s.statusLine, command: inner };
175
- delete sl._ccUsageInstalled;
176
- delete sl._ccUsageOriginal;
177
- next.statusLine = sl;
178
- return next;
179
- }
180
- const wrapMatch = cmd.match(/^sh -c '(.*); (?:echo -n|printf) " "; cc-usage-fetch[^']*'$/s);
181
- if (wrapMatch) {
182
- const inner = wrapMatch[1].replace(/'\\''/g, "'");
183
- const next = { ...s };
184
- const sl = { ...s.statusLine, command: inner };
185
- delete sl._ccUsageInstalled;
186
- delete sl._ccUsageOriginal;
187
- next.statusLine = sl;
188
- return next;
189
- }
201
+ const wrapFormPosix = cmd.match(/^cc-usage-bar-wrap --prefix='((?:[^']|'\\'')*)'.*$/s);
202
+ if (wrapFormPosix)
203
+ return restoreCommand(s, wrapFormPosix[1].replace(/'\\''/g, "'"));
204
+ const wrapFormWin = cmd.match(/^cc-usage-bar-wrap --prefix="((?:\\.|[^"])*)".*$/s);
205
+ if (wrapFormWin)
206
+ return restoreCommand(s, wrapFormWin[1].replace(/\\"/g, '"'));
207
+ const printfDollarForm = cmd.match(/^sh -c 'printf "%s " "\$\((.*)\)"; cc-usage-fetch.*'$/s);
208
+ if (printfDollarForm)
209
+ return restoreCommand(s, printfDollarForm[1].replace(/'\\''/g, "'"));
210
+ const echoForm = cmd.match(/^sh -c '(.*); (?:echo -n|printf) " "; cc-usage-fetch[^']*'$/s);
211
+ if (echoForm)
212
+ return restoreCommand(s, echoForm[1].replace(/'\\''/g, "'"));
190
213
  const trimmed = cmd.trim();
191
- if (trimmed === exports.FETCH_BIN || trimmed.startsWith(exports.FETCH_BIN + ' ')) {
214
+ if (trimmed === exports.FETCH_BIN ||
215
+ trimmed.startsWith(exports.FETCH_BIN + ' ') ||
216
+ trimmed === exports.WRAP_BIN ||
217
+ trimmed.startsWith(exports.WRAP_BIN + ' ')) {
192
218
  const next = { ...s };
193
219
  delete next.statusLine;
194
220
  return next;
package/dist/src/token.js CHANGED
@@ -61,6 +61,57 @@ function tryKeychain() {
61
61
  return null;
62
62
  }
63
63
  }
64
+ const WIN_CRED_TARGET = 'Claude Code-credentials';
65
+ const WIN_PS_SCRIPT = `$ErrorActionPreference='Stop';try{$src=@'
66
+ using System;
67
+ using System.Runtime.InteropServices;
68
+ using System.Text;
69
+ public static class CcuCred {
70
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
71
+ private struct CRED {
72
+ public uint Flags;
73
+ public uint Type;
74
+ public IntPtr TargetName;
75
+ public IntPtr Comment;
76
+ public long LastWritten;
77
+ public uint BlobSize;
78
+ public IntPtr Blob;
79
+ public uint Persist;
80
+ public uint AttributeCount;
81
+ public IntPtr Attributes;
82
+ public IntPtr TargetAlias;
83
+ public IntPtr UserName;
84
+ }
85
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
86
+ private static extern bool CredRead(string target, int type, int flag, out IntPtr cred);
87
+ [DllImport("advapi32.dll", SetLastError=true)]
88
+ private static extern void CredFree(IntPtr cred);
89
+ public static string Read(string target) {
90
+ IntPtr p;
91
+ if (!CredRead(target, 1, 0, out p)) return null;
92
+ try {
93
+ var c = (CRED)Marshal.PtrToStructure(p, typeof(CRED));
94
+ if (c.BlobSize == 0 || c.Blob == IntPtr.Zero) return null;
95
+ byte[] buf = new byte[c.BlobSize];
96
+ Marshal.Copy(c.Blob, buf, 0, (int)c.BlobSize);
97
+ return Encoding.Unicode.GetString(buf);
98
+ } finally {
99
+ CredFree(p);
100
+ }
101
+ }
102
+ }
103
+ '@;Add-Type -TypeDefinition $src -Language CSharp | Out-Null;$v=[CcuCred]::Read('${WIN_CRED_TARGET}');if($v){[Console]::Out.Write($v)}}catch{exit 1}`;
104
+ function tryWinCredentialManager() {
105
+ if (process.platform !== 'win32')
106
+ return null;
107
+ try {
108
+ const out = (0, node_child_process_1.execFileSync)('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', WIN_PS_SCRIPT], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, encoding: 'utf8', windowsHide: true });
109
+ return extractToken(out);
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
64
115
  function tryFile() {
65
116
  const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
66
117
  try {
@@ -74,6 +125,9 @@ function getToken() {
74
125
  const fromKeychain = tryKeychain();
75
126
  if (fromKeychain)
76
127
  return { token: fromKeychain, source: 'keychain' };
128
+ const fromWinCred = tryWinCredentialManager();
129
+ if (fromWinCred)
130
+ return { token: fromWinCred, source: 'wincred' };
77
131
  const fromFile = tryFile();
78
132
  if (fromFile)
79
133
  return { token: fromFile, source: 'file' };