cc-usage-bar 0.1.0 → 0.4.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.
@@ -1,42 +1,80 @@
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
+ exports.COLOR_MAP = {
27
+ none: '',
28
+ dim: '\x1b[2m',
29
+ black: '\x1b[30m',
30
+ red: '\x1b[31m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ blue: '\x1b[34m',
34
+ magenta: '\x1b[35m',
35
+ cyan: '\x1b[36m',
36
+ white: '\x1b[37m',
37
+ gray: '\x1b[90m',
38
+ boldRed: '\x1b[1;31m',
39
+ boldGreen: '\x1b[1;32m',
40
+ boldYellow: '\x1b[1;33m',
41
+ boldBlue: '\x1b[1;34m',
42
+ boldMagenta: '\x1b[1;35m',
43
+ boldCyan: '\x1b[1;36m',
44
+ boldWhite: '\x1b[1;37m',
45
+ };
46
+ exports.COLOR_NAMES = Object.keys(exports.COLOR_MAP);
47
+ exports.DEFAULT_RAMP = [
48
+ { min: 0, ansi: exports.COLOR_MAP.green },
49
+ { min: 60, ansi: exports.COLOR_MAP.yellow },
50
+ { min: 85, ansi: exports.COLOR_MAP.red },
51
+ ];
13
52
  exports.DEFAULT_FORMAT = {
14
- format: 'compact',
53
+ format: 'bar-countdown',
15
54
  barWidth: 10,
16
55
  color: true,
17
56
  showProviderName: true,
57
+ colorRamp5h: exports.DEFAULT_RAMP,
58
+ colorRampWk: exports.DEFAULT_RAMP,
59
+ colorRampBalance: exports.DEFAULT_RAMP,
18
60
  };
19
61
  const SUB_PRESET_TPL = {
20
62
  compact: '{label} {percent}%',
21
63
  numeric: '{percent}%',
22
64
  time: '{percent}% until {expiry}',
65
+ countdown: '{percent}% in {countdown}',
23
66
  bar: '[{bar}] {percent}%',
24
67
  'bar-time': '[{bar}] {percent}% until {expiry}',
68
+ 'bar-countdown': '[{bar}] {percent}% in {countdown}',
25
69
  };
26
70
  const BAL_PRESET_TPL = {
27
71
  compact: '{amount}',
28
72
  numeric: '{amount}',
29
73
  time: '{amount}',
74
+ countdown: '{amount}',
30
75
  bar: '[{bar}] {amount}',
31
76
  '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',
77
+ 'bar-countdown': '[{bar}] {amount}',
40
78
  };
41
79
  const pad2 = (n) => n.toString().padStart(2, '0');
42
80
  function formatExpiry(iso, now = new Date()) {
@@ -57,6 +95,26 @@ function formatExpiry(iso, now = new Date()) {
57
95
  }
58
96
  return `${target.getFullYear()}-${pad2(target.getMonth() + 1)}-${pad2(target.getDate())}`;
59
97
  }
98
+ function formatCountdown(iso, now = new Date()) {
99
+ if (!iso)
100
+ return '?';
101
+ const target = new Date(iso);
102
+ if (Number.isNaN(target.getTime()))
103
+ return '?';
104
+ const secs = Math.max(0, Math.floor((target.getTime() - now.getTime()) / 1000));
105
+ if (secs < 60)
106
+ return `${secs}s`;
107
+ const mins = Math.floor(secs / 60);
108
+ if (mins < 60)
109
+ return `${mins}m`;
110
+ const hrs = Math.floor(mins / 60);
111
+ const remMin = mins % 60;
112
+ if (hrs < 24)
113
+ return remMin > 0 ? `${hrs}h${remMin}m` : `${hrs}h`;
114
+ const days = Math.floor(hrs / 24);
115
+ const remHr = hrs % 24;
116
+ return remHr > 0 ? `${days}d${remHr}h` : `${days}d`;
117
+ }
60
118
  function isNonEmptyString(v) {
61
119
  return typeof v === 'string' && v.length > 0;
62
120
  }
@@ -100,6 +158,71 @@ function parseBarSpec(raw) {
100
158
  }
101
159
  return null;
102
160
  }
161
+ function hexToAnsi(hex) {
162
+ const m = hex.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
163
+ if (!m)
164
+ return null;
165
+ const h = m[1];
166
+ let r;
167
+ let g;
168
+ let b;
169
+ if (h.length === 3) {
170
+ r = parseInt(h[0] + h[0], 16);
171
+ g = parseInt(h[1] + h[1], 16);
172
+ b = parseInt(h[2] + h[2], 16);
173
+ }
174
+ else {
175
+ r = parseInt(h.slice(0, 2), 16);
176
+ g = parseInt(h.slice(2, 4), 16);
177
+ b = parseInt(h.slice(4, 6), 16);
178
+ }
179
+ return `\x1b[38;2;${r};${g};${b}m`;
180
+ }
181
+ function parseColorRamp(spec) {
182
+ if (!spec)
183
+ return null;
184
+ const parts = spec.split(',').map((s) => s.trim()).filter(Boolean);
185
+ if (parts.length === 0)
186
+ return null;
187
+ const rules = [];
188
+ for (const p of parts) {
189
+ const m = p.match(/^(\d+(?:\.\d+)?):(\S+)$/);
190
+ if (!m)
191
+ return null;
192
+ const min = parseFloat(m[1]);
193
+ const token = m[2];
194
+ let ansi;
195
+ if (token === 'none') {
196
+ ansi = null;
197
+ }
198
+ else if (token.startsWith('#')) {
199
+ ansi = hexToAnsi(token);
200
+ if (ansi === null)
201
+ return null;
202
+ }
203
+ else if (token in exports.COLOR_MAP) {
204
+ ansi = exports.COLOR_MAP[token] || null;
205
+ }
206
+ else {
207
+ return null;
208
+ }
209
+ rules.push({ min, ansi });
210
+ }
211
+ rules.sort((a, b) => a.min - b.min);
212
+ return rules;
213
+ }
214
+ function colorFromRamp(util, ramp) {
215
+ if (!ramp || ramp.length === 0)
216
+ return null;
217
+ let picked = null;
218
+ for (const r of ramp) {
219
+ if (util >= r.min)
220
+ picked = r.ansi;
221
+ else
222
+ break;
223
+ }
224
+ return picked;
225
+ }
103
226
  function splitText(s) {
104
227
  return Array.from(s);
105
228
  }
@@ -118,7 +241,7 @@ function formatBar(pct, width = 10, spec, activeColor = null, useColor = false)
118
241
  if (!useColor)
119
242
  return active + inactive;
120
243
  const activePart = activeColor && active ? paint(active, activeColor, true) : active;
121
- const inactivePart = inactive && spec.emptyStyle !== 'plain' ? paint(inactive, ANSI.dim, true) : inactive;
244
+ const inactivePart = inactive && spec.emptyStyle !== 'plain' ? paint(inactive, exports.COLOR_MAP.dim, true) : inactive;
122
245
  return activePart + inactivePart;
123
246
  }
124
247
  if (spec?.mode === 'frames') {
@@ -128,20 +251,18 @@ function formatBar(pct, width = 10, spec, activeColor = null, useColor = false)
128
251
  const filled = Math.round((clamped / 100) * width);
129
252
  return '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled));
130
253
  }
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
254
  function paint(text, color, useColor) {
142
255
  if (!useColor || !color)
143
256
  return text;
144
- return `${color}${text}${ANSI.reset}`;
257
+ return `${color}${text}${ANSI_RESET}`;
258
+ }
259
+ function applyTierPaint(text, color, opts, tpl) {
260
+ if (opts.barSpec?.mode === 'tint' && opts.color && tpl.includes('{bar}'))
261
+ return text;
262
+ return paint(text, color, opts.color);
263
+ }
264
+ function prependProvider(text, planName, opts) {
265
+ return opts.showProviderName && planName ? `${planName} ${text}` : text;
145
266
  }
146
267
  function applyTemplate(tpl, vars) {
147
268
  return tpl.replace(/\{(\w+)\}/g, (_, k) => (k in vars ? vars[k] : `{${k}}`));
@@ -149,17 +270,17 @@ function applyTemplate(tpl, vars) {
149
270
  function renderTier(ctx, opts) {
150
271
  const pct = Math.round(ctx.tier.utilization);
151
272
  const tpl = opts.template ?? SUB_PRESET_TPL[opts.format];
152
- const color = ctx.label === '5h' ? fiveHourColor(pct) : weekColor(pct);
273
+ const ramp = ctx.label === '5h' ? opts.colorRamp5h ?? exports.DEFAULT_RAMP : opts.colorRampWk ?? exports.DEFAULT_RAMP;
274
+ const color = colorFromRamp(pct, ramp);
153
275
  const text = applyTemplate(tpl, {
154
276
  label: ctx.label,
155
277
  percent: String(pct),
156
278
  bar: formatBar(pct, opts.barWidth, opts.barSpec, color, opts.color),
157
279
  expiry: formatExpiry(ctx.tier.resets_at, opts.now),
280
+ countdown: formatCountdown(ctx.tier.resets_at, opts.now),
158
281
  provider: ctx.provider,
159
282
  });
160
- if (opts.barSpec?.mode === 'tint' && opts.color && tpl.includes('{bar}'))
161
- return text;
162
- return paint(text, color, opts.color);
283
+ return applyTierPaint(text, color, opts, tpl);
163
284
  }
164
285
  function currencySymbol(unit) {
165
286
  switch (unit.toUpperCase()) {
@@ -186,35 +307,35 @@ function renderSubscription(d, opts) {
186
307
  if (parts.length === 0)
187
308
  return '';
188
309
  const sep = opts.format === 'compact' && !opts.template ? ' ' : ' / ';
189
- const body = parts.join(sep);
190
- return opts.showProviderName && d.planName ? `${d.planName} ${body}` : body;
310
+ return prependProvider(parts.join(sep), d.planName, opts);
191
311
  }
192
312
  function renderBalance(d, opts) {
193
313
  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;
314
+ return prependProvider(paint(d.invalidMessage, exports.COLOR_MAP.red, opts.color), d.planName, opts);
315
+ }
316
+ if (typeof d.total !== 'number') {
317
+ const tpl = opts.template ?? BAL_PRESET_TPL.compact;
318
+ const text = applyTemplate(tpl, {
319
+ label: '', percent: '0', bar: '', expiry: '', countdown: '',
320
+ provider: d.planName ?? '',
321
+ amount: fmtMoney(d.remaining, d.unit),
322
+ });
323
+ return prependProvider(applyTierPaint(text, null, opts, tpl), d.planName, opts);
324
+ }
325
+ const total = d.total;
326
+ const usedPct = total > 0 ? ((total - d.remaining) / total) * 100 : 0;
203
327
  const tpl = opts.template ?? BAL_PRESET_TPL[opts.format];
204
- const color = typeof d.total === 'number' ? fiveHourColor(usedPct) : null;
328
+ const color = colorFromRamp(usedPct, opts.colorRampBalance ?? exports.DEFAULT_RAMP);
205
329
  const text = applyTemplate(tpl, {
206
330
  label: '',
207
331
  percent: String(Math.round(usedPct)),
208
- bar: typeof d.total === 'number' ? formatBar(usedPct, opts.barWidth, opts.barSpec, color, opts.color) : '',
332
+ bar: formatBar(usedPct, opts.barWidth, opts.barSpec, color, opts.color),
209
333
  expiry: '',
334
+ countdown: '',
210
335
  provider: d.planName ?? '',
211
- amount,
336
+ amount: `${fmtMoney(d.remaining, d.unit)}/${fmtMoney(total, d.unit)}`,
212
337
  });
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;
338
+ return prependProvider(applyTierPaint(text, color, opts, tpl), d.planName, opts);
218
339
  }
219
340
  function renderUsage(data, opts) {
220
341
  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' };