sidekick-agent-hub 0.18.2 → 0.18.4

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.
@@ -18926,6 +18926,29 @@ var require_quotaSnapshots = __commonJS({
18926
18926
  ensureConfigDir();
18927
18927
  atomicWriteJson(getQuotaSnapshotPath(), store);
18928
18928
  }
18929
+ function snapshotTimeMs(quota) {
18930
+ const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : NaN;
18931
+ return Number.isFinite(capturedAt) ? capturedAt : 0;
18932
+ }
18933
+ function windowResetMs(value) {
18934
+ const ms = Date.parse(value);
18935
+ return Number.isFinite(ms) ? ms : 0;
18936
+ }
18937
+ function shouldKeepExistingSnapshot(existing, next) {
18938
+ const existingPrimaryReset = windowResetMs(existing.fiveHour.resetsAt);
18939
+ const nextPrimaryReset = windowResetMs(next.fiveHour.resetsAt);
18940
+ if (existingPrimaryReset !== nextPrimaryReset)
18941
+ return existingPrimaryReset > nextPrimaryReset;
18942
+ const existingSecondaryReset = windowResetMs(existing.sevenDay.resetsAt);
18943
+ const nextSecondaryReset = windowResetMs(next.sevenDay.resetsAt);
18944
+ if (existingSecondaryReset !== nextSecondaryReset)
18945
+ return existingSecondaryReset > nextSecondaryReset;
18946
+ const existingUtilization = existing.fiveHour.utilization + existing.sevenDay.utilization;
18947
+ const nextUtilization = next.fiveHour.utilization + next.sevenDay.utilization;
18948
+ if (existingUtilization !== nextUtilization)
18949
+ return existingUtilization > nextUtilization;
18950
+ return snapshotTimeMs(existing) > snapshotTimeMs(next);
18951
+ }
18929
18952
  function writeQuotaSnapshot(providerId, accountId, quota) {
18930
18953
  const store = readStore();
18931
18954
  const snapshot = {
@@ -18936,6 +18959,9 @@ var require_quotaSnapshots = __commonJS({
18936
18959
  stale: false
18937
18960
  };
18938
18961
  const index = store.snapshots.findIndex((item) => item.providerId === providerId && item.accountId === accountId);
18962
+ if (index >= 0 && shouldKeepExistingSnapshot(store.snapshots[index].quota, snapshot)) {
18963
+ return;
18964
+ }
18939
18965
  const record = {
18940
18966
  providerId,
18941
18967
  accountId,
@@ -18963,6 +18989,356 @@ var require_quotaSnapshots = __commonJS({
18963
18989
  }
18964
18990
  });
18965
18991
 
18992
+ // ../sidekick-shared/dist/quotaHistory.js
18993
+ var require_quotaHistory = __commonJS({
18994
+ "../sidekick-shared/dist/quotaHistory.js"(exports) {
18995
+ "use strict";
18996
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
18997
+ if (k2 === void 0) k2 = k;
18998
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18999
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19000
+ desc = { enumerable: true, get: function() {
19001
+ return m[k];
19002
+ } };
19003
+ }
19004
+ Object.defineProperty(o, k2, desc);
19005
+ } : function(o, m, k, k2) {
19006
+ if (k2 === void 0) k2 = k;
19007
+ o[k2] = m[k];
19008
+ });
19009
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
19010
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19011
+ } : function(o, v) {
19012
+ o["default"] = v;
19013
+ });
19014
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
19015
+ var ownKeys = function(o) {
19016
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
19017
+ var ar = [];
19018
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
19019
+ return ar;
19020
+ };
19021
+ return ownKeys(o);
19022
+ };
19023
+ return function(mod) {
19024
+ if (mod && mod.__esModule) return mod;
19025
+ var result = {};
19026
+ if (mod != null) {
19027
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
19028
+ }
19029
+ __setModuleDefault(result, mod);
19030
+ return result;
19031
+ };
19032
+ }();
19033
+ Object.defineProperty(exports, "__esModule", { value: true });
19034
+ exports.getWorkspaceIdFromPath = getWorkspaceIdFromPath2;
19035
+ exports.appendQuotaHistorySample = appendQuotaHistorySample;
19036
+ exports.readQuotaHistoryRange = readQuotaHistoryRange;
19037
+ exports.readQuotaHistoryDailyBuckets = readQuotaHistoryDailyBuckets2;
19038
+ exports.pruneQuotaHistory = pruneQuotaHistory;
19039
+ exports._resetQuotaHistoryInMemoryStateForTests = _resetQuotaHistoryInMemoryStateForTests;
19040
+ var crypto = __importStar(__require("crypto"));
19041
+ var fs9 = __importStar(__require("fs"));
19042
+ var path8 = __importStar(__require("path"));
19043
+ var paths_1 = require_paths();
19044
+ var quotaSnapshots_1 = require_quotaSnapshots();
19045
+ var DEFAULT_MIN_INTERVAL_MS = 6e4;
19046
+ var DEFAULT_RETENTION_DAYS = 91;
19047
+ var PRUNE_FILESIZE_THRESHOLD = 16 * 1024;
19048
+ var MS_PER_DAY2 = 864e5;
19049
+ var appendChains = /* @__PURE__ */ new Map();
19050
+ var lastWriteCache = /* @__PURE__ */ new Map();
19051
+ function getWorkspaceIdFromPath2(workspacePath) {
19052
+ let resolved;
19053
+ try {
19054
+ resolved = fs9.realpathSync(workspacePath);
19055
+ } catch {
19056
+ resolved = path8.resolve(workspacePath);
19057
+ }
19058
+ return crypto.createHash("sha256").update(resolved).digest("hex").slice(0, 16);
19059
+ }
19060
+ function getHistoryFilePath(workspaceId, provider) {
19061
+ return path8.join((0, paths_1.getConfigDir)(), "quota-history", workspaceId, `${provider}.jsonl`);
19062
+ }
19063
+ function ensureHistoryDir(workspaceId) {
19064
+ fs9.mkdirSync(path8.join((0, paths_1.getConfigDir)(), "quota-history", workspaceId), { recursive: true, mode: 448 });
19065
+ }
19066
+ function parseSampleLine(line) {
19067
+ const trimmed = line.trim();
19068
+ if (!trimmed)
19069
+ return null;
19070
+ try {
19071
+ const parsed = JSON.parse(trimmed);
19072
+ if (parsed && typeof parsed === "object" && typeof parsed.timestamp === "string" && parsed.fiveHour && parsed.sevenDay) {
19073
+ return parsed;
19074
+ }
19075
+ return null;
19076
+ } catch {
19077
+ return null;
19078
+ }
19079
+ }
19080
+ function readLastSampleTimestampMs(filePath) {
19081
+ let fd;
19082
+ try {
19083
+ fd = fs9.openSync(filePath, "r");
19084
+ const stat = fs9.fstatSync(fd);
19085
+ if (stat.size === 0)
19086
+ return null;
19087
+ const chunkSize = 4096;
19088
+ let remaining = stat.size;
19089
+ let buffer = Buffer.alloc(0);
19090
+ while (remaining > 0) {
19091
+ const readSize = Math.min(chunkSize, remaining);
19092
+ const chunk = Buffer.alloc(readSize);
19093
+ fs9.readSync(fd, chunk, 0, readSize, remaining - readSize);
19094
+ buffer = Buffer.concat([chunk, buffer]);
19095
+ remaining -= readSize;
19096
+ const text = buffer.toString("utf8");
19097
+ const trimmedRight = text.replace(/\n+$/, "");
19098
+ const lastNewline = trimmedRight.lastIndexOf("\n");
19099
+ if (lastNewline >= 0) {
19100
+ const lastLine = trimmedRight.slice(lastNewline + 1);
19101
+ const sample = parseSampleLine(lastLine);
19102
+ return sample ? Date.parse(sample.timestamp) || null : null;
19103
+ }
19104
+ if (remaining === 0) {
19105
+ const sample = parseSampleLine(trimmedRight);
19106
+ return sample ? Date.parse(sample.timestamp) || null : null;
19107
+ }
19108
+ }
19109
+ return null;
19110
+ } catch (err) {
19111
+ if (err?.code === "ENOENT")
19112
+ return null;
19113
+ return null;
19114
+ } finally {
19115
+ if (fd !== void 0) {
19116
+ try {
19117
+ fs9.closeSync(fd);
19118
+ } catch {
19119
+ }
19120
+ }
19121
+ }
19122
+ }
19123
+ function atomicRewriteFile(filePath, contents) {
19124
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.${crypto.randomBytes(8).toString("hex")}.tmp`;
19125
+ try {
19126
+ fs9.writeFileSync(tmp, contents, { encoding: "utf8", mode: 384 });
19127
+ fs9.renameSync(tmp, filePath);
19128
+ } catch (error) {
19129
+ try {
19130
+ fs9.rmSync(tmp, { force: true });
19131
+ } catch {
19132
+ }
19133
+ throw error;
19134
+ }
19135
+ }
19136
+ function pruneFileSync(filePath, retentionDays) {
19137
+ let stat;
19138
+ try {
19139
+ stat = fs9.statSync(filePath);
19140
+ } catch {
19141
+ return { kept: 0, pruned: 0 };
19142
+ }
19143
+ if (stat.size === 0)
19144
+ return { kept: 0, pruned: 0 };
19145
+ const cutoffMs = Date.now() - retentionDays * MS_PER_DAY2;
19146
+ const raw = fs9.readFileSync(filePath, "utf8");
19147
+ const lines = raw.split("\n");
19148
+ const keptLines = [];
19149
+ let pruned = 0;
19150
+ for (const line of lines) {
19151
+ if (!line.trim())
19152
+ continue;
19153
+ const sample = parseSampleLine(line);
19154
+ if (!sample) {
19155
+ pruned += 1;
19156
+ continue;
19157
+ }
19158
+ const ts = Date.parse(sample.timestamp);
19159
+ if (Number.isFinite(ts) && ts >= cutoffMs) {
19160
+ keptLines.push(line);
19161
+ } else {
19162
+ pruned += 1;
19163
+ }
19164
+ }
19165
+ if (pruned > 0) {
19166
+ atomicRewriteFile(filePath, keptLines.length > 0 ? keptLines.join("\n") + "\n" : "");
19167
+ }
19168
+ return { kept: keptLines.length, pruned };
19169
+ }
19170
+ function sampleToQuotaState(sample) {
19171
+ const providerId = sample.runtimeProvider === "claude" ? "claude-code" : "codex";
19172
+ return {
19173
+ fiveHour: { utilization: sample.fiveHour.utilization, resetsAt: sample.fiveHour.resetsAt },
19174
+ sevenDay: { utilization: sample.sevenDay.utilization, resetsAt: sample.sevenDay.resetsAt },
19175
+ available: sample.available,
19176
+ error: sample.error,
19177
+ providerId,
19178
+ source: sample.source ?? "session",
19179
+ capturedAt: sample.timestamp,
19180
+ stale: sample.stale
19181
+ };
19182
+ }
19183
+ async function runAppend(sample, filePath, options) {
19184
+ let lastTs = lastWriteCache.get(filePath);
19185
+ if (lastTs === void 0) {
19186
+ const fromDisk = readLastSampleTimestampMs(filePath);
19187
+ if (fromDisk !== null) {
19188
+ lastTs = fromDisk;
19189
+ lastWriteCache.set(filePath, fromDisk);
19190
+ }
19191
+ }
19192
+ const sampleTs = Date.parse(sample.timestamp);
19193
+ if (lastTs !== void 0 && Number.isFinite(sampleTs) && sampleTs - lastTs < options.minIntervalMs) {
19194
+ return;
19195
+ }
19196
+ ensureHistoryDir(sample.workspaceId);
19197
+ const line = JSON.stringify(sample) + "\n";
19198
+ await fs9.promises.appendFile(filePath, line, { encoding: "utf8", mode: 384 });
19199
+ lastWriteCache.set(filePath, Number.isFinite(sampleTs) ? sampleTs : Date.now());
19200
+ try {
19201
+ const stat = await fs9.promises.stat(filePath);
19202
+ if (stat.size >= PRUNE_FILESIZE_THRESHOLD) {
19203
+ pruneFileSync(filePath, options.retentionDays);
19204
+ }
19205
+ } catch {
19206
+ }
19207
+ try {
19208
+ const providerId = sample.runtimeProvider === "claude" ? "claude-code" : "codex";
19209
+ (0, quotaSnapshots_1.writeQuotaSnapshot)(providerId, sample.providerId, sampleToQuotaState(sample));
19210
+ } catch {
19211
+ }
19212
+ }
19213
+ async function appendQuotaHistorySample(sample, options = {}) {
19214
+ const resolved = {
19215
+ minIntervalMs: options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS,
19216
+ retentionDays: options.retentionDays ?? DEFAULT_RETENTION_DAYS
19217
+ };
19218
+ const filePath = getHistoryFilePath(sample.workspaceId, sample.runtimeProvider);
19219
+ const previous = appendChains.get(filePath) ?? Promise.resolve();
19220
+ const next = previous.then(() => runAppend(sample, filePath, resolved)).catch(() => {
19221
+ });
19222
+ appendChains.set(filePath, next);
19223
+ try {
19224
+ await next;
19225
+ } finally {
19226
+ if (appendChains.get(filePath) === next) {
19227
+ appendChains.delete(filePath);
19228
+ }
19229
+ }
19230
+ }
19231
+ function defaultRangeMs() {
19232
+ const toMs = Date.now();
19233
+ const fromMs = toMs - DEFAULT_RETENTION_DAYS * MS_PER_DAY2;
19234
+ return { fromMs, toMs };
19235
+ }
19236
+ async function readQuotaHistoryRange(options) {
19237
+ const filePath = getHistoryFilePath(options.workspaceId, options.provider);
19238
+ let raw;
19239
+ try {
19240
+ raw = await fs9.promises.readFile(filePath, "utf8");
19241
+ } catch (err) {
19242
+ if (err?.code === "ENOENT")
19243
+ return [];
19244
+ throw err;
19245
+ }
19246
+ const { fromMs: defaultFromMs, toMs: defaultToMs } = defaultRangeMs();
19247
+ const fromMs = options.from ? Date.parse(options.from) : defaultFromMs;
19248
+ const toMs = options.to ? Date.parse(options.to) : defaultToMs;
19249
+ const samples = [];
19250
+ for (const line of raw.split("\n")) {
19251
+ const sample = parseSampleLine(line);
19252
+ if (!sample)
19253
+ continue;
19254
+ const ts = Date.parse(sample.timestamp);
19255
+ if (!Number.isFinite(ts))
19256
+ continue;
19257
+ if (ts < fromMs || ts > toMs)
19258
+ continue;
19259
+ samples.push(sample);
19260
+ }
19261
+ samples.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
19262
+ return samples;
19263
+ }
19264
+ function utcDateString(ms) {
19265
+ return new Date(ms).toISOString().slice(0, 10);
19266
+ }
19267
+ function addDaysUtc(dateString, days) {
19268
+ const ms = Date.parse(`${dateString}T00:00:00Z`) + days * MS_PER_DAY2;
19269
+ return utcDateString(ms);
19270
+ }
19271
+ async function readQuotaHistoryDailyBuckets2(options) {
19272
+ const samples = await readQuotaHistoryRange(options);
19273
+ const { fromMs: defaultFromMs, toMs: defaultToMs } = defaultRangeMs();
19274
+ const fromMs = options.from ? Date.parse(options.from) : defaultFromMs;
19275
+ const toMs = options.to ? Date.parse(options.to) : defaultToMs;
19276
+ const startDate = utcDateString(fromMs);
19277
+ const endDate = utcDateString(toMs);
19278
+ const grouped = /* @__PURE__ */ new Map();
19279
+ for (const sample of samples) {
19280
+ const day = sample.timestamp.slice(0, 10);
19281
+ const bucket = grouped.get(day);
19282
+ if (bucket) {
19283
+ bucket.push(sample);
19284
+ } else {
19285
+ grouped.set(day, [sample]);
19286
+ }
19287
+ }
19288
+ const buckets = [];
19289
+ let cursor = startDate;
19290
+ for (let i = 0; i <= 366 && cursor <= endDate; i += 1) {
19291
+ const daySamples = grouped.get(cursor);
19292
+ if (!daySamples || daySamples.length === 0) {
19293
+ buckets.push({
19294
+ date: cursor,
19295
+ samples: 0,
19296
+ maxUtilizationFiveHour: 0,
19297
+ maxUtilizationSevenDay: 0,
19298
+ avgUtilizationFiveHour: 0,
19299
+ avgUtilizationSevenDay: 0,
19300
+ anyUnavailable: false
19301
+ });
19302
+ } else {
19303
+ let maxFive = 0;
19304
+ let maxSeven = 0;
19305
+ let sumFive = 0;
19306
+ let sumSeven = 0;
19307
+ let anyUnavailable = false;
19308
+ for (const s of daySamples) {
19309
+ maxFive = Math.max(maxFive, s.fiveHour.utilization);
19310
+ maxSeven = Math.max(maxSeven, s.sevenDay.utilization);
19311
+ sumFive += s.fiveHour.utilization;
19312
+ sumSeven += s.sevenDay.utilization;
19313
+ if (!s.available)
19314
+ anyUnavailable = true;
19315
+ }
19316
+ const n = daySamples.length;
19317
+ buckets.push({
19318
+ date: cursor,
19319
+ samples: n,
19320
+ maxUtilizationFiveHour: maxFive,
19321
+ maxUtilizationSevenDay: maxSeven,
19322
+ avgUtilizationFiveHour: Math.round(sumFive / n * 100) / 100,
19323
+ avgUtilizationSevenDay: Math.round(sumSeven / n * 100) / 100,
19324
+ anyUnavailable
19325
+ });
19326
+ }
19327
+ cursor = addDaysUtc(cursor, 1);
19328
+ }
19329
+ return buckets;
19330
+ }
19331
+ async function pruneQuotaHistory(workspaceId, provider, retentionDays = DEFAULT_RETENTION_DAYS) {
19332
+ const filePath = getHistoryFilePath(workspaceId, provider);
19333
+ return pruneFileSync(filePath, retentionDays);
19334
+ }
19335
+ function _resetQuotaHistoryInMemoryStateForTests() {
19336
+ appendChains.clear();
19337
+ lastWriteCache.clear();
19338
+ }
19339
+ }
19340
+ });
19341
+
18966
19342
  // ../sidekick-shared/dist/codexQuota.js
18967
19343
  var require_codexQuota = __commonJS({
18968
19344
  "../sidekick-shared/dist/codexQuota.js"(exports) {
@@ -19018,6 +19394,33 @@ var require_codexQuota = __commonJS({
19018
19394
  var DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
19019
19395
  var DEFAULT_MAX_SESSION_FILES = 50;
19020
19396
  var CHATGPT_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
19397
+ function timestampMs(value, fallbackMs = 0) {
19398
+ if (!value)
19399
+ return fallbackMs;
19400
+ const ms = Date.parse(value);
19401
+ return Number.isFinite(ms) ? ms : fallbackMs;
19402
+ }
19403
+ function isPreferredQuotaHit(candidate, current) {
19404
+ if (!current)
19405
+ return true;
19406
+ const candidatePrimaryReset = timestampMs(candidate.quota.fiveHour.resetsAt);
19407
+ const currentPrimaryReset = timestampMs(current.quota.fiveHour.resetsAt);
19408
+ if (candidatePrimaryReset !== currentPrimaryReset)
19409
+ return candidatePrimaryReset > currentPrimaryReset;
19410
+ const candidateSecondaryReset = timestampMs(candidate.quota.sevenDay.resetsAt);
19411
+ const currentSecondaryReset = timestampMs(current.quota.sevenDay.resetsAt);
19412
+ if (candidateSecondaryReset !== currentSecondaryReset)
19413
+ return candidateSecondaryReset > currentSecondaryReset;
19414
+ const candidateUtilization = candidate.quota.fiveHour.utilization + candidate.quota.sevenDay.utilization;
19415
+ const currentUtilization = current.quota.fiveHour.utilization + current.quota.sevenDay.utilization;
19416
+ if (candidateUtilization !== currentUtilization)
19417
+ return candidateUtilization > currentUtilization;
19418
+ const candidateMs = timestampMs(candidate.quota.capturedAt, candidate.mtimeMs);
19419
+ const currentMs = timestampMs(current.quota.capturedAt, current.mtimeMs);
19420
+ if (candidateMs !== currentMs)
19421
+ return candidateMs > currentMs;
19422
+ return candidate.mtimeMs > current.mtimeMs;
19423
+ }
19021
19424
  function normalizePercent(value) {
19022
19425
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
19023
19426
  }
@@ -19137,14 +19540,52 @@ var require_codexQuota = __commonJS({
19137
19540
  };
19138
19541
  }
19139
19542
  function readLatestCodexQuotaFromRollouts(sessionPaths, options = {}) {
19543
+ return readLatestCodexQuotaHitFromRollouts(sessionPaths, options)?.quota ?? null;
19544
+ }
19545
+ function readLatestCodexQuotaHitFromRollouts(sessionPaths, options = {}) {
19140
19546
  const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
19141
19547
  const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
19548
+ let latest = null;
19142
19549
  for (const sessionPath of sessionPaths.slice(0, maxSessionFiles)) {
19143
19550
  const hit = readLatestQuotaFromRollout(sessionPath, maxTailBytes, options.source ?? "session");
19144
- if (hit)
19145
- return hit.quota;
19551
+ if (hit && isPreferredQuotaHit(hit, latest)) {
19552
+ latest = hit;
19553
+ }
19146
19554
  }
19147
- return null;
19555
+ return latest;
19556
+ }
19557
+ function dedupePaths(paths) {
19558
+ const seen = /* @__PURE__ */ new Set();
19559
+ const unique = [];
19560
+ for (const filePath of paths) {
19561
+ if (seen.has(filePath))
19562
+ continue;
19563
+ seen.add(filePath);
19564
+ unique.push(filePath);
19565
+ }
19566
+ return unique;
19567
+ }
19568
+ function sortPathsByMtimeDesc(paths) {
19569
+ return [...paths].sort((a, b) => {
19570
+ const aMtime = safeMtimeMs(a);
19571
+ const bMtime = safeMtimeMs(b);
19572
+ return bMtime - aMtime;
19573
+ });
19574
+ }
19575
+ function safeMtimeMs(filePath) {
19576
+ try {
19577
+ return fs9.statSync(filePath).mtime.getTime();
19578
+ } catch {
19579
+ return 0;
19580
+ }
19581
+ }
19582
+ function findAccountRolloutFiles(codexHome) {
19583
+ const homes = codexHome ? [codexHome] : (0, codexProfiles_1.getCodexMonitoringHomes)();
19584
+ const files = [];
19585
+ for (const home of homes) {
19586
+ files.push(...findRolloutFiles(path8.join(home, "sessions")));
19587
+ }
19588
+ return sortPathsByMtimeDesc(dedupePaths(files));
19148
19589
  }
19149
19590
  function resolveCodexQuotaFromLocalSources(options = {}) {
19150
19591
  const account = options.activeAccount !== void 0 ? options.activeAccount : (0, codexProfiles_1.getActiveCodexAccount)();
@@ -19158,20 +19599,22 @@ var require_codexQuota = __commonJS({
19158
19599
  return new codex_1.CodexProvider();
19159
19600
  })();
19160
19601
  try {
19161
- const workspaceSessions = options.workspacePath ? provider.findAllSessions(options.workspacePath) : [];
19162
- const workspaceQuota = readLatestCodexQuotaFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
19163
- if (workspaceQuota) {
19164
- if (account)
19165
- writeSnapshot("codex", account.id, workspaceQuota);
19166
- return enrichCodexQuota(workspaceQuota, account);
19167
- }
19168
- const codexHome = options.codexHome ?? (0, codexProfiles_1.resolveSidekickCodexHome)();
19169
- const accountSessions = findRolloutFiles(path8.join(codexHome, "sessions"));
19170
- const accountQuota = readLatestCodexQuotaFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
19171
- if (accountQuota) {
19602
+ const candidates = [];
19603
+ if (options.workspacePath) {
19604
+ const workspaceSessions = provider.findAllSessions(options.workspacePath);
19605
+ const workspaceHit = readLatestCodexQuotaHitFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
19606
+ if (workspaceHit)
19607
+ candidates.push(workspaceHit);
19608
+ }
19609
+ const accountSessions = findAccountRolloutFiles(options.codexHome);
19610
+ const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
19611
+ if (accountHit)
19612
+ candidates.push(accountHit);
19613
+ const latestHit = candidates.reduce((latest, candidate) => isPreferredQuotaHit(candidate, latest) ? candidate : latest, null);
19614
+ if (latestHit) {
19172
19615
  if (account)
19173
- writeSnapshot("codex", account.id, accountQuota);
19174
- return enrichCodexQuota(accountQuota, account);
19616
+ writeSnapshot("codex", account.id, latestHit.quota);
19617
+ return enrichCodexQuota(latestHit.quota, account);
19175
19618
  }
19176
19619
  const cached = account ? readSnapshot("codex", account.id) : null;
19177
19620
  if (cached) {
@@ -19329,7 +19772,7 @@ var require_codexQuota = __commonJS({
19329
19772
  continue;
19330
19773
  const quota = quotaFromCodexRateLimits2(parsed.payload.rate_limits, source, parsed.timestamp ?? new Date(stat.mtime).toISOString());
19331
19774
  if (quota)
19332
- return { quota, filePath: sessionPath };
19775
+ return { quota, filePath: sessionPath, mtimeMs: stat.mtime.getTime() };
19333
19776
  } catch {
19334
19777
  }
19335
19778
  }
@@ -19425,6 +19868,7 @@ var require_codexQuotaWatcher = __commonJS({
19425
19868
  var codexProfiles_1 = require_codexProfiles();
19426
19869
  var codexQuota_1 = require_codexQuota();
19427
19870
  var quotaSnapshots_1 = require_quotaSnapshots();
19871
+ var quotaHistory_1 = require_quotaHistory();
19428
19872
  var codex_1 = require_codex();
19429
19873
  var DEFAULT_DISCOVERY_POLL_INTERVAL_MS = 3e4;
19430
19874
  function accountEmail(account) {
@@ -19463,6 +19907,8 @@ var require_codexQuotaWatcher = __commonJS({
19463
19907
  watchFile;
19464
19908
  maxTailBytes;
19465
19909
  maxSessionFiles;
19910
+ workspaceId;
19911
+ appendHistorySample;
19466
19912
  listeners = [];
19467
19913
  discoveryTimer;
19468
19914
  provider = null;
@@ -19481,6 +19927,8 @@ var require_codexQuotaWatcher = __commonJS({
19481
19927
  this.watchFile = options.watchFile ?? fs9.watch;
19482
19928
  this.maxTailBytes = options.maxTailBytes;
19483
19929
  this.maxSessionFiles = options.maxSessionFiles;
19930
+ this.workspaceId = options.workspaceId;
19931
+ this.appendHistorySample = options.appendHistorySample ?? quotaHistory_1.appendQuotaHistorySample;
19484
19932
  }
19485
19933
  start() {
19486
19934
  if (this.running)
@@ -19577,6 +20025,28 @@ var require_codexQuotaWatcher = __commonJS({
19577
20025
  const account = this.getActiveAccount();
19578
20026
  if (account) {
19579
20027
  this.writeSnapshot("codex", account.id, liveQuota);
20028
+ if (this.workspaceId) {
20029
+ const sample = {
20030
+ timestamp: liveQuota.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
20031
+ runtimeProvider: "codex",
20032
+ providerId: account.id,
20033
+ workspaceId: this.workspaceId,
20034
+ fiveHour: { utilization: liveQuota.fiveHour.utilization, resetsAt: liveQuota.fiveHour.resetsAt },
20035
+ sevenDay: { utilization: liveQuota.sevenDay.utilization, resetsAt: liveQuota.sevenDay.resetsAt },
20036
+ available: liveQuota.available,
20037
+ error: liveQuota.error,
20038
+ source: liveQuota.source,
20039
+ stale: liveQuota.stale
20040
+ };
20041
+ try {
20042
+ const result = this.appendHistorySample(sample);
20043
+ if (result && typeof result.catch === "function") {
20044
+ result.catch(() => {
20045
+ });
20046
+ }
20047
+ } catch {
20048
+ }
20049
+ }
19580
20050
  }
19581
20051
  this.emitState(enrichQuotaState({
19582
20052
  ...liveQuota,
@@ -19660,6 +20130,9 @@ var require_peakHours = __commonJS({
19660
20130
  "../sidekick-shared/dist/peakHours.js"(exports) {
19661
20131
  "use strict";
19662
20132
  Object.defineProperty(exports, "__esModule", { value: true });
20133
+ exports.isClaudeCodeSessionProvider = isClaudeCodeSessionProvider2;
20134
+ exports.createPeakHoursNotApplicableState = createPeakHoursNotApplicableState2;
20135
+ exports.scopePeakHoursToSessionProvider = scopePeakHoursToSessionProvider;
19663
20136
  exports.fetchPeakHoursStatus = fetchPeakHoursStatus4;
19664
20137
  var PROMOCLOCK_ENDPOINT = "https://promoclock.co/api/status";
19665
20138
  function unavailableState() {
@@ -19676,6 +20149,35 @@ var require_peakHours = __commonJS({
19676
20149
  unavailable: true
19677
20150
  };
19678
20151
  }
20152
+ var PROVIDER_DISPLAY_NAMES2 = {
20153
+ "claude-code": "Claude Code",
20154
+ opencode: "OpenCode",
20155
+ codex: "Codex CLI"
20156
+ };
20157
+ function isClaudeCodeSessionProvider2(providerId) {
20158
+ return providerId === "claude-code";
20159
+ }
20160
+ function createPeakHoursNotApplicableState2(providerId) {
20161
+ const providerName = PROVIDER_DISPLAY_NAMES2[providerId] ?? providerId;
20162
+ return {
20163
+ status: "unknown",
20164
+ isPeak: false,
20165
+ sessionLimitSpeed: "unknown",
20166
+ label: "Claude peak hours not applicable",
20167
+ peakHoursDescription: "",
20168
+ nextChange: null,
20169
+ minutesUntilChange: null,
20170
+ note: `Claude peak hours apply only to Claude Code sessions, not ${providerName}.`,
20171
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20172
+ unavailable: true,
20173
+ notApplicable: true
20174
+ };
20175
+ }
20176
+ function scopePeakHoursToSessionProvider(providerId, status) {
20177
+ if (!isClaudeCodeSessionProvider2(providerId))
20178
+ return null;
20179
+ return status ?? null;
20180
+ }
19679
20181
  function normalizeStatus(raw) {
19680
20182
  if (raw === "peak" || raw === "off_peak")
19681
20183
  return raw;
@@ -36417,8 +36919,8 @@ var require_dist = __commonJS({
36417
36919
  Object.defineProperty(exports, "__esModule", { value: true });
36418
36920
  exports.findActiveClaudeSession = exports.discoverSessionDirectory = exports.getClaudeSessionDirectory = exports.encodeClaudeWorkspacePath = exports.detectSessionActivity = exports.extractTaskInfo = exports.scanSubagentDir = exports.normalizeCodexToolInput = exports.normalizeCodexToolName = exports.extractPatchFilePaths = exports.CodexRolloutParser = exports.parseDbPartData = exports.parseDbMessageData = exports.convertOpenCodeMessage = exports.detectPlanModeFromText = exports.normalizeToolInput = exports.normalizeToolName = exports.TRUNCATION_PATTERNS = exports.JsonlParser = exports.CodexProvider = exports.OpenCodeProvider = exports.ClaudeCodeProvider = exports.getAllDetectedProviders = exports.detectProvider = exports.readClaudeCodePlanFiles = exports.getPlanAnalytics = exports.writePlans = exports.getLatestPlan = exports.readPlans = exports.readLatestHandoff = exports.readHistory = exports.readNotes = exports.readDecisions = exports.readTasks = exports.getProjectSlugRaw = exports.getProjectSlug = exports.encodeWorkspacePath = exports.getGlobalDataPath = exports.getProjectDataPath = exports.getConfigDir = exports.MAX_PLANS_PER_PROJECT = exports.PLAN_SCHEMA_VERSION = exports.createEmptyTokenTotals = exports.HISTORICAL_DATA_SCHEMA_VERSION = exports.STALENESS_THRESHOLDS = exports.IMPORTANCE_DECAY_FACTORS = exports.KNOWLEDGE_NOTE_SCHEMA_VERSION = exports.DECISION_LOG_SCHEMA_VERSION = exports.normalizeTaskStatus = exports.TASK_PERSISTENCE_SCHEMA_VERSION = void 0;
36419
36921
  exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.PHRASE_CATEGORIES = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.formatTokenCount = exports.formatDurationMs = exports.createJsonlTail = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.composeContext = exports.FilterEngine = exports.searchSessions = exports.CodexDatabase = exports.OpenCodeDatabase = exports.discoverDebugLogs = exports.collapseDuplicates = exports.filterByLevel = exports.parseDebugLog = exports.scanSubagentTraces = exports.findAllSessionsWithWorktrees = exports.discoverWorktreeSiblings = exports.resolveWorktreeMainRepo = exports.getAllClaudeProjectFolders = exports.decodeEncodedPath = exports.getMostRecentlyActiveSessionDir = exports.findSubdirectorySessionDirs = exports.findSessionsInDirectory = exports.findAllClaudeSessions = void 0;
36420
- exports.fetchCodexQuotaFromApi = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.fetchQuota = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = exports.HeatmapTracker = exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = void 0;
36421
- exports.fetchPeakHoursStatus = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = void 0;
36922
+ exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.fetchQuota = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = exports.HeatmapTracker = exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = void 0;
36923
+ exports.scopePeakHoursToSessionProvider = exports.isClaudeCodeSessionProvider = exports.fetchPeakHoursStatus = exports.createPeakHoursNotApplicableState = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = exports.fetchCodexQuotaFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = void 0;
36422
36924
  var taskPersistence_1 = require_taskPersistence();
36423
36925
  Object.defineProperty(exports, "TASK_PERSISTENCE_SCHEMA_VERSION", { enumerable: true, get: function() {
36424
36926
  return taskPersistence_1.TASK_PERSISTENCE_SCHEMA_VERSION;
@@ -36923,6 +37425,22 @@ var require_dist = __commonJS({
36923
37425
  Object.defineProperty(exports, "writeQuotaSnapshot", { enumerable: true, get: function() {
36924
37426
  return quotaSnapshots_1.writeQuotaSnapshot;
36925
37427
  } });
37428
+ var quotaHistory_1 = require_quotaHistory();
37429
+ Object.defineProperty(exports, "appendQuotaHistorySample", { enumerable: true, get: function() {
37430
+ return quotaHistory_1.appendQuotaHistorySample;
37431
+ } });
37432
+ Object.defineProperty(exports, "readQuotaHistoryRange", { enumerable: true, get: function() {
37433
+ return quotaHistory_1.readQuotaHistoryRange;
37434
+ } });
37435
+ Object.defineProperty(exports, "readQuotaHistoryDailyBuckets", { enumerable: true, get: function() {
37436
+ return quotaHistory_1.readQuotaHistoryDailyBuckets;
37437
+ } });
37438
+ Object.defineProperty(exports, "pruneQuotaHistory", { enumerable: true, get: function() {
37439
+ return quotaHistory_1.pruneQuotaHistory;
37440
+ } });
37441
+ Object.defineProperty(exports, "getWorkspaceIdFromPath", { enumerable: true, get: function() {
37442
+ return quotaHistory_1.getWorkspaceIdFromPath;
37443
+ } });
36926
37444
  var codexQuota_1 = require_codexQuota();
36927
37445
  Object.defineProperty(exports, "fetchCodexQuotaFromApi", { enumerable: true, get: function() {
36928
37446
  return codexQuota_1.fetchCodexQuotaFromApi;
@@ -37033,9 +37551,18 @@ var require_dist = __commonJS({
37033
37551
  return providerStatus_1.fetchOpenAIStatus;
37034
37552
  } });
37035
37553
  var peakHours_1 = require_peakHours();
37554
+ Object.defineProperty(exports, "createPeakHoursNotApplicableState", { enumerable: true, get: function() {
37555
+ return peakHours_1.createPeakHoursNotApplicableState;
37556
+ } });
37036
37557
  Object.defineProperty(exports, "fetchPeakHoursStatus", { enumerable: true, get: function() {
37037
37558
  return peakHours_1.fetchPeakHoursStatus;
37038
37559
  } });
37560
+ Object.defineProperty(exports, "isClaudeCodeSessionProvider", { enumerable: true, get: function() {
37561
+ return peakHours_1.isClaudeCodeSessionProvider;
37562
+ } });
37563
+ Object.defineProperty(exports, "scopePeakHoursToSessionProvider", { enumerable: true, get: function() {
37564
+ return peakHours_1.scopePeakHoursToSessionProvider;
37565
+ } });
37039
37566
  }
37040
37567
  });
37041
37568
 
@@ -39271,7 +39798,7 @@ var init_UpdateCheckService = __esm({
39271
39798
  /** Run the update check (one-shot). */
39272
39799
  async check() {
39273
39800
  try {
39274
- const current = "0.18.2";
39801
+ const current = "0.18.4";
39275
39802
  const cached = this.readCache();
39276
39803
  let latest;
39277
39804
  if (cached && Date.now() - cached.checkedAt < CACHE_TTL_MS) {
@@ -79888,7 +80415,7 @@ function StatusBar({
79888
80415
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { children: parseBlessedTags(BRAND_INLINE) }),
79889
80416
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Text, { dimColor: true, children: [
79890
80417
  " v",
79891
- "0.18.2"
80418
+ "0.18.4"
79892
80419
  ] }),
79893
80420
  updateInfo && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Text, { color: "yellow", children: [
79894
80421
  " (v",
@@ -80278,7 +80805,7 @@ function ChangelogOverlay({ entries, scrollOffset }) {
80278
80805
  " ",
80279
80806
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Text, { bold: true, color: "cyan", children: [
80280
80807
  "Terminal Dashboard v",
80281
- "0.18.2"
80808
+ "0.18.4"
80282
80809
  ] }),
80283
80810
  latestDate ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Text, { color: "gray", children: [
80284
80811
  " \u2014 ",
@@ -80600,7 +81127,7 @@ var init_mouse = __esm({
80600
81127
  var CHANGELOG_default;
80601
81128
  var init_CHANGELOG = __esm({
80602
81129
  "CHANGELOG.md"() {
80603
- CHANGELOG_default = '# Changelog\n\nAll notable changes to the Sidekick Agent Hub CLI will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [0.18.2] - 2026-05-19\n\n### Added\n\n- **`sidekick quota --refresh`**: New flag on the `quota` command that, for Codex, explicitly refreshes from the ChatGPT usage API before falling back to local rollout data and cached snapshots. Without the flag, the Codex quota path stays fully local and makes no upstream network call\n\n### Changed\n\n- **Codex quota is local-only by default**: `sidekick quota --provider codex` now delegates to the new `resolveCodexQuota` orchestrator in `sidekick-shared`. It checks the current workspace\'s most recent rollout, then recent account-level rollouts under `CODEX_HOME/sessions`, then the active account\'s cached snapshot \u2014 no upstream network call unless `--refresh` is passed. Failure output continues to include structured `failureKind` / `httpStatus` / `retryAfterMs` fields under `--json`\n- **Bundled `sidekick-shared` 0.18.2**: Picks up the new Codex quota orchestrator (`resolveCodexQuota`, `resolveCodexQuotaFromLocalSources`, `readLatestCodexQuotaFromRollouts`, `fetchCodexQuotaFromApi`), the relaxed `CodexRateLimits` shape (nullable `resets_at` / `window_minutes`), the rate-limit-only `token_count` event emission in `JsonlSessionWatcher`, and `state_N.sqlite` discovery in `CodexDatabase` + provider auto-detect\n\n## [0.18.1] - 2026-05-08\n\n### Changed\n\n- **Shared dashboard formatting**: terminal dashboard `fmtNum()` and `formatDuration()` now delegate to `formatTokenCount()` and `formatDurationMs()` from `sidekick-shared`, keeping the existing CLI surface (uppercase `K`/`M` suffix, compact `1m5s` style) while removing forked rounding logic\n\n## [0.18.0] - 2026-05-08\n\n### Changed\n\n- **Bundled `sidekick-shared` 0.18.0**: Picks up the new provider-aware quota orchestration surface \u2014 `MultiProviderQuotaService`, `CodexQuotaWatcher`, `getActiveAccountStatus()`, `extractToolCall()`, cost-provenance helpers (`calculateCostWithProvenance`, `mergeCostSources`), and model display helpers (`shortModelName`, `getModelDisplayInfo`, `compareModelIds`, `sortModelIds`). `parseModelId()` also now recognizes legacy Claude IDs such as `claude-3-opus-20240229` and `claude-3-5-sonnet-20241022`\n- **No CLI runtime changes**: This release ships the shared library upgrade for downstream tooling alignment; `sidekick quota`, `sidekick status`, and the live dashboard keep using the existing polling path. Wiring the new orchestrator into the CLI will land in a follow-up release\n\n## [0.17.7] - 2026-04-28\n\n### Fixed\n\n- **Quota snapshot write race**: Updated the bundled `sidekick-shared` snapshot writer so concurrent `sidekick quota` / Codex session updates no longer collide on `quota-snapshots.json.tmp` or throw `ENOENT`. Failed writes now also clean up their partial temp files instead of leaving orphans in `~/.config/sidekick/`\n\n## [0.17.6] - 2026-04-19\n\n### Added\n\n- **`sidekick peak` command**: One-shot check for Claude\'s current peak-hours state \u2014 weekdays 13:00\u201319:00 UTC, when session limits drain faster on Free/Pro/Max/Team subscriptions. Prints a color-coded status block with a countdown to the next transition. Data comes from the public `promoclock.co/api/status` endpoint (third-party, unaffiliated with Anthropic) with a graceful fallback when unreachable. `--json` emits the full raw state\n- **Peak-hours block in `sidekick status`**: When the active provider is `claude-code`, the Claude + OpenAI health blocks are now followed by a **Claude Peak Hours** block (off-peak or in-peak, with countdown). Gated on the provider so OpenCode / Codex users don\'t trigger an unnecessary third-party fetch. `--json` output includes the new `peak` field\n- **Peak-hours summary in `sidekick quota`**: Claude subscription quota output now shows a **Peak** line under the 5-hour / 7-day bars \u2014 green dot off-peak, orange dot during an active peak, with a countdown to the next transition. `--json` output includes the new `peak` field\n\n## [0.17.5] - 2026-04-18\n\n### Added\n\n- **Default account bootstrap at CLI startup**: The CLI now calls `ensureDefaultAccounts()` from `sidekick-shared` at module load and awaits the result inside a Commander `preAction` hook, so the first real subcommand blocks briefly on the bootstrap while `--version` and `--help` stay instant. When a system Claude Code or Codex credential exists and no saved account is active for that provider yet, the CLI registers it as "Default" \u2014 `sidekick quota`, `sidekick account`, and `sidekick stats` now reflect the active account on first run without requiring an explicit `sidekick account --add` first. Idempotent, never overwrites manually saved accounts, and all errors are swallowed so startup is never blocked\n\nThanks to [@B33pBeeps](https://github.com/B33pBeeps) (Juan Fourie) for contributing this feature in [#16](https://github.com/cesarandreslopez/sidekick-agent-hub/pull/16).\n\n## [0.17.4] - 2026-04-17\n\n### Changed\n\n- **Pricing hydration import migrated to `sidekick-shared/node`**: `cli.ts` now imports `hydratePricingCatalog` from the new Node-only subpath and keeps `detectProvider` on the package root. Runtime behavior is unchanged; the split makes the CLI\'s import surface self-documenting (hydration is explicitly a Node API) and aligns the CLI with the shared library\'s new versioned public API contract\n\n## [0.17.3] - 2026-04-17\n\n### Changed\n\n- **Version sync with the VS Code extension**: Republished to keep CLI, extension, and shared-library versions aligned after a cosmetic changelog fix in 0.17.3. No CLI code changes \u2014 functionally identical to 0.17.2\n\n## [0.17.2] - 2026-04-17\n\n### Added\n\n- **LiteLLM pricing hydration on startup**: The CLI now fetches the LiteLLM pricing catalog on startup and caches to `~/.config/sidekick/pricing-catalog.json` with a 24-hour TTL, 3s timeout, and stale-cache fallback \u2014 new model prices are picked up without a CLI upgrade\n- **Expanded pricing coverage**: GPT-4o, GPT-4.1, GPT-5.x, o1, o3, and o3-mini families are now priced alongside the existing Claude entries\n- **Real-dollar Codex / Claude Code costs**: `EventAggregator` computes cost from the pricing table when the session provider doesn\'t report one, so `sidekick` live dashboards now show actual dollars for Codex and Claude Code sessions\n- **`stats` footer lists unpriced models**: `sidekick stats` prints any models encountered with no pricing entry so missing coverage is visible\n\n### Fixed\n\n- **Context-gauge % wrong for Opus 4.7 (1M) and other new models**: The dashboard\'s context gauge was dividing by 200K for Claude Opus 4.7 (native 1M), inflating the displayed %. The shared model \u2192 context-window map now includes Opus/Sonnet 4.7 (1M), GPT-5.4 (1.05M), GPT-5.3-Codex (400K), and GPT-5.3-Codex-Spark (128K). Claude Code\'s `[1m]` suffix is now also honored as an explicit 1M marker\n- **Silent Sonnet-priced fallback for unknown models**: Codex, GPT-5.x, and o-series rows were being rendered at Sonnet rates. Unknown-model rows now render as `\u2014` in yellow instead of inventing a dollar figure\n\n### Changed\n\n- **`historical-data.json` schema v2**: reads `priced` flag and `unpricedModelIds` from records written by the latest VS Code extension; v1 records still read correctly\n\n## [0.17.1] - 2026-04-13\n\n### Fixed\n\n- **Codex multi-home session discovery**: Provider detection now scans all candidate Codex home directories, fixing missed sessions when the managed profile home is empty but the system `~/.codex/` has activity\n\n## [0.17.0] - 2026-04-13\n\n### Added\n\n- **Multi-provider account management**: `sidekick account` now supports `--provider codex` for Codex profile management alongside Claude Code accounts\n- **Codex account lifecycle**: `--add` prepares a profile and spawns `codex login`; `--switch-to` and `--remove` accept email, label, or profile ID\n- **Quota snapshot fallback**: `sidekick quota` for Codex shows cached rate-limit snapshots when no active session exists, with "cached from" timestamp\n\n### Fixed\n\n- **Email normalization**: Claude account lookup normalizes email case for reliable matching\n\n## [0.16.1] - 2026-03-27\n\n### Fixed\n\n- **Dashboard provider status scoping**: The TUI now shows degraded-service notices only for the monitored provider \u2014 Claude for Claude Code sessions, OpenAI for Codex sessions, and no status banner for OpenCode\n\n## [0.16.0] - 2026-03-23\n\n### Changed\n\n- **Consistent cost formatting**: All cost displays (`stats`, `context`, Sessions panel, narrative prompt) now use shared `formatCost()` with intelligent decimal precision (4 places for < $0.01, 2 otherwise)\n- **QuotaService**: Rewritten to wrap shared `QuotaPoller` with exponential backoff instead of manual polling loop\n- **modelContext**: Now re-exports `getModelInfo` from shared library alongside `getContextWindowSize`\n\n## [0.15.2] - 2026-03-18\n\n### Fixed\n\n- **CLI help descriptions**: Updated `quota` and `status` command descriptions to reflect provider-aware behavior\n- **`sidekick quota --provider`**: Added local `--provider` option so `sidekick quota --provider codex` works naturally\n\n## [0.15.0] - 2026-03-18\n\n### Added\n\n- **OpenAI status page monitoring**: CLI dashboard now shows OpenAI API status alongside Claude API status\n- **Codex rate limits in dashboard**: Sessions panel displays Codex rate-limit data with "Rate Limits" header instead of "Quota"\n- **Provider-aware `sidekick quota` command**: Detects active provider and shows Codex rate limits, Claude subscription quota, or an informational message for OpenCode\n\n### Fixed\n\n- **QuotaService polling for Codex**: Dashboard no longer starts Claude OAuth quota polling when the active provider is Codex\n\n## [0.14.2] - 2026-03-16\n\n### Fixed\n\n- **Quota polling interval**: Reduced quota refresh from every 30 seconds to every 5 minutes to avoid unnecessary API calls\n- **SessionsPanel `detailWidth()` call**: Removed unused parameter from `detailWidth()` in the Sessions panel quota rendering\n\n## [0.14.1] - 2026-03-14\n\n### Fixed\n\n- **Per-model context window sizes**: Dashboard context gauge now shows correct utilization for Claude Opus 4.6 (1M context) and other models with non-200K windows\n\n### Changed\n\n- **Shared model context lookup**: CLI dashboard now uses the centralized `getModelContextWindowSize()` from `sidekick-shared` instead of a local duplicate map\n\n## [0.14.0] - 2026-03-12\n\n### Added\n\n- **`sidekick account` Command**: Manage Claude Code accounts from the terminal \u2014 list saved accounts, add the current account with an optional label, switch to the next or a specific account, and remove accounts. Supports `--json` output for scripting\n- **Quota Account Label**: `sidekick quota` now shows the active account email and label above the quota bars when multi-account is enabled\n- **macOS Keychain Support**: `sidekick account` and `sidekick quota` now read and write credentials via the system Keychain on macOS, fixing account switching and quota checks on Mac\n\n## [0.13.8] - 2026-03-12\n\n### Changed\n\n- **Structured quota failure output**: `sidekick quota` now renders consistent auth, rate-limit, server, network, and unexpected-failure copy from shared quota failure descriptors while preserving `--json` machine-readable output\n- **Dashboard unavailable quota rendering**: The Sessions panel now shows Claude Code quota failures inline instead of hiding the quota section whenever subscription data is unavailable\n- **Quota transition toasts**: The Ink dashboard now fires low-noise toast notifications only when Claude Code quota failure state changes, avoiding repeated alerts every polling interval\n\n## [0.13.7] - 2026-03-11\n\n### Changed\n\n- **npm README sync**: Updated the published CLI package README to reflect current OpenCode monitoring behavior, platform-specific data directories, and the `sqlite3` runtime requirement\n- **README badge cleanup**: Removed the Ask DeepWiki badge from the published CLI package README; the repo root README still keeps it\n\n## [0.13.6] - 2026-03-11\n\n### Changed\n\n- **Refreshed CLI Dashboard Wordmark**: Updated the dashboard wordmark/header styling for a cleaner splash and dashboard identity\n\n### Fixed\n\n- **OpenCode dashboard startup**: OpenCode DB-backed session discovery now resolves projects by worktree, sandboxes, and session directory instead of quietly behaving like no session exists\n- **OpenCode runtime notices**: The CLI now prints an OpenCode-only actionable notice when `opencode.db` exists but `sqlite3` is missing, blocked, or otherwise unusable in the current shell environment\n\n## [0.13.5] - 2026-03-10\n\n### Added\n\n- **`sidekick status` Command**: One-shot Claude API status check with color-coded text output and `--json` mode\n- **Dashboard Status Banner**: Status bar shows a colored `\u25CF API minor/major/critical` indicator when Claude is degraded; Sessions panel Summary tab shows an "API Status" section with affected components and active incident details. Polls every 60s\n\n## [0.13.4] - 2026-03-08\n\n### Fixed\n\n- **Onboarding Phrase Spam**: Splash screen and detail pane motivational phrases memoized \u2014 no longer flicker every render tick (fixes [#13](https://github.com/cesarandreslopez/sidekick-agent-hub/issues/13))\n\n### Changed\n\n- **Simplified Logo**: Replaced 6-line ASCII robot art with compact text header in splash, help, and changelog overlays\n- **Removed Dead Code**: Removed unused `getSplashContent()` and `HELP_HEADER` exports from branding module\n\n## [0.13.3] - 2026-03-04\n\n_No CLI-specific changes in this release._\n\n## [0.13.2] - 2026-03-04\n\n_No CLI-specific changes in this release._\n\n## [0.13.1] - 2026-03-04\n\n### Added\n\n- **`sidekick quota` Command**: One-shot subscription quota check showing 5-hour and 7-day utilization with color-coded progress bars and reset countdowns \u2014 supports `--json` for machine-readable output\n- **Quota Projections**: Elapsed-time projections shown in `sidekick quota` output and TUI dashboard quota section \u2014 displays projected end-of-window utilization next to current value (e.g., `40% \u2192 100%`), included in `--json` output as `projectedFiveHour` / `projectedSevenDay`\n\n## [0.13.0] - 2026-03-03\n\n_No CLI-specific changes in this release._\n\n## [0.12.10] - 2026-03-01\n\n### Added\n\n- **Events Panel** (key 7): Scrollable live event stream with colored type badges (`[USR]`, `[AST]`, `[TOOL]`, `[RES]`), timestamps, and keyword-highlighted summaries; detail tabs for full event JSON and surrounding context\n- **Charts Panel** (key 8): Tool frequency horizontal bars, event type distribution, 60-minute activity heatmap using `\u2591\u2592\u2593\u2588` intensity characters, and pattern analysis with frequency bars and template text\n- **Multi-Mode Filter**: `/` filter overlay now supports four modes \u2014 substring, fuzzy, regex, and date range \u2014 Tab cycles modes, regex mode shows red validation errors\n- **Search Term Highlighting**: Active filter terms highlighted in blue within side list items\n- **Timeline Keyword Coloring**: Event summaries in the Sessions panel Timeline tab now use semantic keyword coloring \u2014 errors red, success green, tool names cyan, file paths magenta\n\n### Removed\n\n- **Search Panel**: Removed redundant Search panel (previously key 7) \u2014 the `/` filter with multi-mode support serves the same purpose\n\n## [0.12.9] - 2026-02-28\n\n### Added\n\n- **Standalone Data Commands**: `sidekick tasks`, `sidekick decisions`, `sidekick notes`, `sidekick stats`, `sidekick handoff` for accessing project data without launching the TUI\n- **`sidekick search <query>`**: Cross-session full-text search from the terminal\n- **`sidekick context`**: Composite output of tasks, decisions, notes, and handoff for piping into other tools\n- **`--list` flag on `sidekick dump`**: Discover available session IDs before requiring `--session <id>`\n- **Search Panel**: Search panel (panel 7) wired into the TUI dashboard\n\n### Changed\n\n- **`taskMerger` utility**: Duplicate `mergeTasks` logic extracted into shared `taskMerger` utility\n- **Model constants**: Hardcoded model IDs extracted to named constants\n\n### Fixed\n\n- **`convention` icon**: Notes panel icon replaced with valid `tip` type\n- **Linux clipboard**: Now supports Wayland (`wl-copy`) and `xsel` fallbacks, with error messages instead of silent failure\n- **`provider.dispose()`**: Added to `dump` and `report` commands (prevents SQLite connection leaks)\n\n## [0.12.8] - 2026-02-28\n\n### Changed\n\n- **Dashboard UI/UX Polish**: Visual overhaul for better hierarchy, consistency, and readability\n - Splash screen and help overlay now display the robot ASCII logo\n - Toast notifications show severity icons (\u2718 error, \u26A0 warning, \u25CF info) with inner padding\n - Focused pane uses double-border for clear focus indication\n - Section dividers (`\u2500\u2500 Title \u2500\u2500\u2500\u2500`) replace bare bold headers in summary, agents, and context attribution\n - Tab bar: active tab underlined in magenta, inactive tabs dimmed, bracket syntax removed\n - Status bar: segmented layout with `\u2502` separators; keys bold, labels dim\n - Summary metrics condensed: elapsed/events/compactions on one line, tokens on one line with cache rate and cost\n - Sparklines display peak metadata annotations\n - Progress bars use blessed color tags for consistent coloring\n - Help overlay uses dot-leader alignment for all keybinding rows\n - Empty state hints per panel (e.g. "Tasks appear as your agent works.")\n - Session picker groups sessions by provider with section headers when multiple providers are present\n\n## [0.12.7] - 2026-02-27\n\n### Added\n\n- **HTML Session Report**: `sidekick report` command generates a self-contained HTML report and opens it in the default browser\n - Options: `--session`, `--output`, `--theme` (dark/light), `--no-open`, `--no-thinking`\n - TUI Dashboard: press `r` to generate and open an HTML report for the current session\n\n## [0.12.6] - 2026-02-26\n\n### Added\n\n- **Session Dump Command**: `sidekick dump` exports session data in text, markdown, or JSON format with `--format`, `--width`, and `--expand` options\n- **Plans Panel Re-enabled**: Plans panel restored in CLI dashboard with plan file discovery from `~/.claude/plans/`\n- **Enhanced Status Bar**: Session info display improved with richer metadata\n\n### Fixed\n\n- **Old snapshot format migration**: Restoring pre-0.12.3 session snapshots no longer shows empty timeline entries\n\n### Changed\n\n- **Phrase library moved to shared**: CLI-specific phrase formatting kept local, all phrase content now from `sidekick-shared`\n\n## [0.12.5] - 2026-02-24\n\n### Fixed\n\n- **Update check too slow to notice new versions**: Reduced npm registry cache TTL from 24 hours to 4 hours so upgrade notices appear sooner after a new release\n\n## [0.12.4] - 2026-02-24\n\n### Fixed\n\n- **Session crash on upgrade**: Fixed `d.timestamp.getTime is not a function` error when restoring tool call data from session snapshots \u2014 `Date` objects were serialized to strings by JSON but not rehydrated on restore, causing the session monitor to crash on first run after upgrading from 0.12.2 to 0.12.3\n\n## [0.12.3] - 2026-02-24\n\n### Added\n\n- **Latest-node indicator**: The most recently added node in tree and boxed mind map views is now marked with a yellow indicator\n- **Plan analytics in mind map**: Tree and boxed views now display plan progress and per-step metrics\n - Tree view: plan header shows completion stats; steps show complexity, duration, tokens, tool calls, and errors in metadata brackets\n - Box view: progress bar with completion percentage; steps show right-aligned metrics; subtitle shows step count and total duration\n- **Cross-provider plan extraction**: Shared `PlanExtractor` now handles Claude Code (EnterPlanMode/ExitPlanMode) and OpenCode (`<proposed_plan>` XML) plans \u2014 previously only Codex plans were shown\n- **Enriched plan data model**: Plan steps include duration, token count, tool call count, and error messages\n- **Phase-grouped plan display**: When a plan has phase structure, tree and boxed views group steps under phase headers with context lines from the original plan markdown\n- **Node type filter**: Press `f` on the Mind Map tab to cycle through node type filters (file, tool, task, subagent, command, plan, knowledge-note) \u2014 non-matching sections render dimmed in grey\n\n### Fixed\n\n- **Kanban board regression**: Subagent and plan-step tasks now correctly appear in the kanban board\n\n### Changed\n\n- **Plans panel temporarily disabled**: The Plans panel in the CLI dashboard is disabled until plan-mode event capture is reliably working end-to-end. Plan nodes in the mind map remain active.\n- `DashboardState` now delegates to shared `EventAggregator` instead of maintaining its own aggregation logic\n\n## [0.12.2] - 2026-02-23\n\n### Added\n\n- **Update notifications**: The dashboard now checks the npm registry for newer versions on startup and shows a yellow banner in the status bar when an update is available (e.g., `v0.13.0 available \u2014 npm i -g sidekick-agent-hub`). Results are cached for 24 hours to avoid repeated network requests.\n\n## [0.12.1] - 2026-02-23\n\n### Fixed\n\n- **VS Code integration**: Fixed exit code 127 when the extension launches the CLI dashboard on systems using nvm or volta (node binary not found when shell init is bypassed)\n\n## [0.12.0] - 2026-02-22\n\n### Added\n\n- **"Open CLI Dashboard" VS Code Integration**: New VS Code command `Sidekick: Open CLI Dashboard` launches the TUI dashboard in an integrated terminal\n - Install the CLI with `npm install -g sidekick-agent-hub`\n\n## [0.11.0] - 2026-02-19\n\n### Added\n\n- **Initial Release**: Full-screen TUI dashboard for monitoring agent sessions from the terminal\n - Ink-based terminal UI with panels for sessions, tasks, kanban, mind map, notes, decisions, search, files, and git diff\n - Multi-provider support: auto-detects Claude Code, OpenCode, and Codex sessions\n - Reads from `~/.config/sidekick/` \u2014 the same data files the VS Code extension writes\n - Usage: `sidekick dashboard [--project <path>] [--provider <id>]`\n';
81130
+ CHANGELOG_default = '# Changelog\n\nAll notable changes to the Sidekick Agent Hub CLI will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [0.18.4] - 2026-05-27\n\n### Added\n\n- **`sidekick peak --provider <id>`**: New flag gates peak-hours output on the session provider. When the resolved provider is not `claude-code`, the command prints a "not applicable" message instead of calling the upstream endpoint\n\n### Changed\n\n- **Bundled `sidekick-shared` 0.18.4**: Picks up `scopePeakHoursToSessionProvider()`, `isClaudeCodeSessionProvider()`, `createPeakHoursNotApplicableState()` for peak-hours scoping, the improved Codex quota snapshot selection logic (`isPreferredQuotaHit`, `findAccountRolloutFiles`, `shouldKeepExistingSnapshot`), and the `notApplicable` field on `PeakHoursState`\n\n## [0.18.3] - 2026-05-19\n\n### Added\n\n- **`sidekick quota history`**: New subcommand that renders a 13-week GitHub-contributions-style heatmap of quota utilization for the current workspace. Flags: `--weeks <n>` (1-26, default 13), `--provider claude|codex` (default both), `--workspace <path>` (default cwd). Bucketed glyphs (`\xB7 \u2591 \u2592 \u2593 \u2588`) are color-coded by utilization band (\u22640 / <25 / <50 / <75 / \u226575), with per-provider rows and a peak / avg / unavailable-days / samples footer. Days that hit `available: false` render as a red `\xD7`. With `--json`, emits a `{ workspaceId, weeks, providers: { claude?, codex? }, generatedAt }` payload \u2014 the same shape consumed by the VS Code dashboard\n\n### Changed\n\n- **Bundled `sidekick-shared` 0.18.3**: Picks up the new per-workspace quota history surface (`appendQuotaHistorySample`, `readQuotaHistoryRange`, `readQuotaHistoryDailyBuckets`, `pruneQuotaHistory`, `getWorkspaceIdFromPath`) and the optional `workspaceId` / `appendHistorySample` hooks on `CodexQuotaWatcher`\n\n## [0.18.2] - 2026-05-19\n\n### Added\n\n- **`sidekick quota --refresh`**: New flag on the `quota` command that, for Codex, explicitly refreshes from the ChatGPT usage API before falling back to local rollout data and cached snapshots. Without the flag, the Codex quota path stays fully local and makes no upstream network call\n\n### Changed\n\n- **Codex quota is local-only by default**: `sidekick quota --provider codex` now delegates to the new `resolveCodexQuota` orchestrator in `sidekick-shared`. It checks the current workspace\'s most recent rollout, then recent account-level rollouts under `CODEX_HOME/sessions`, then the active account\'s cached snapshot \u2014 no upstream network call unless `--refresh` is passed. Failure output continues to include structured `failureKind` / `httpStatus` / `retryAfterMs` fields under `--json`\n- **Bundled `sidekick-shared` 0.18.2**: Picks up the new Codex quota orchestrator (`resolveCodexQuota`, `resolveCodexQuotaFromLocalSources`, `readLatestCodexQuotaFromRollouts`, `fetchCodexQuotaFromApi`), the relaxed `CodexRateLimits` shape (nullable `resets_at` / `window_minutes`), the rate-limit-only `token_count` event emission in `JsonlSessionWatcher`, and `state_N.sqlite` discovery in `CodexDatabase` + provider auto-detect\n\n## [0.18.1] - 2026-05-08\n\n### Changed\n\n- **Shared dashboard formatting**: terminal dashboard `fmtNum()` and `formatDuration()` now delegate to `formatTokenCount()` and `formatDurationMs()` from `sidekick-shared`, keeping the existing CLI surface (uppercase `K`/`M` suffix, compact `1m5s` style) while removing forked rounding logic\n\n## [0.18.0] - 2026-05-08\n\n### Changed\n\n- **Bundled `sidekick-shared` 0.18.0**: Picks up the new provider-aware quota orchestration surface \u2014 `MultiProviderQuotaService`, `CodexQuotaWatcher`, `getActiveAccountStatus()`, `extractToolCall()`, cost-provenance helpers (`calculateCostWithProvenance`, `mergeCostSources`), and model display helpers (`shortModelName`, `getModelDisplayInfo`, `compareModelIds`, `sortModelIds`). `parseModelId()` also now recognizes legacy Claude IDs such as `claude-3-opus-20240229` and `claude-3-5-sonnet-20241022`\n- **No CLI runtime changes**: This release ships the shared library upgrade for downstream tooling alignment; `sidekick quota`, `sidekick status`, and the live dashboard keep using the existing polling path. Wiring the new orchestrator into the CLI will land in a follow-up release\n\n## [0.17.7] - 2026-04-28\n\n### Fixed\n\n- **Quota snapshot write race**: Updated the bundled `sidekick-shared` snapshot writer so concurrent `sidekick quota` / Codex session updates no longer collide on `quota-snapshots.json.tmp` or throw `ENOENT`. Failed writes now also clean up their partial temp files instead of leaving orphans in `~/.config/sidekick/`\n\n## [0.17.6] - 2026-04-19\n\n### Added\n\n- **`sidekick peak` command**: One-shot check for Claude\'s current peak-hours state \u2014 weekdays 13:00\u201319:00 UTC, when session limits drain faster on Free/Pro/Max/Team subscriptions. Prints a color-coded status block with a countdown to the next transition. Data comes from the public `promoclock.co/api/status` endpoint (third-party, unaffiliated with Anthropic) with a graceful fallback when unreachable. `--json` emits the full raw state\n- **Peak-hours block in `sidekick status`**: When the active provider is `claude-code`, the Claude + OpenAI health blocks are now followed by a **Claude Peak Hours** block (off-peak or in-peak, with countdown). Gated on the provider so OpenCode / Codex users don\'t trigger an unnecessary third-party fetch. `--json` output includes the new `peak` field\n- **Peak-hours summary in `sidekick quota`**: Claude subscription quota output now shows a **Peak** line under the 5-hour / 7-day bars \u2014 green dot off-peak, orange dot during an active peak, with a countdown to the next transition. `--json` output includes the new `peak` field\n\n## [0.17.5] - 2026-04-18\n\n### Added\n\n- **Default account bootstrap at CLI startup**: The CLI now calls `ensureDefaultAccounts()` from `sidekick-shared` at module load and awaits the result inside a Commander `preAction` hook, so the first real subcommand blocks briefly on the bootstrap while `--version` and `--help` stay instant. When a system Claude Code or Codex credential exists and no saved account is active for that provider yet, the CLI registers it as "Default" \u2014 `sidekick quota`, `sidekick account`, and `sidekick stats` now reflect the active account on first run without requiring an explicit `sidekick account --add` first. Idempotent, never overwrites manually saved accounts, and all errors are swallowed so startup is never blocked\n\nThanks to [@B33pBeeps](https://github.com/B33pBeeps) (Juan Fourie) for contributing this feature in [#16](https://github.com/cesarandreslopez/sidekick-agent-hub/pull/16).\n\n## [0.17.4] - 2026-04-17\n\n### Changed\n\n- **Pricing hydration import migrated to `sidekick-shared/node`**: `cli.ts` now imports `hydratePricingCatalog` from the new Node-only subpath and keeps `detectProvider` on the package root. Runtime behavior is unchanged; the split makes the CLI\'s import surface self-documenting (hydration is explicitly a Node API) and aligns the CLI with the shared library\'s new versioned public API contract\n\n## [0.17.3] - 2026-04-17\n\n### Changed\n\n- **Version sync with the VS Code extension**: Republished to keep CLI, extension, and shared-library versions aligned after a cosmetic changelog fix in 0.17.3. No CLI code changes \u2014 functionally identical to 0.17.2\n\n## [0.17.2] - 2026-04-17\n\n### Added\n\n- **LiteLLM pricing hydration on startup**: The CLI now fetches the LiteLLM pricing catalog on startup and caches to `~/.config/sidekick/pricing-catalog.json` with a 24-hour TTL, 3s timeout, and stale-cache fallback \u2014 new model prices are picked up without a CLI upgrade\n- **Expanded pricing coverage**: GPT-4o, GPT-4.1, GPT-5.x, o1, o3, and o3-mini families are now priced alongside the existing Claude entries\n- **Real-dollar Codex / Claude Code costs**: `EventAggregator` computes cost from the pricing table when the session provider doesn\'t report one, so `sidekick` live dashboards now show actual dollars for Codex and Claude Code sessions\n- **`stats` footer lists unpriced models**: `sidekick stats` prints any models encountered with no pricing entry so missing coverage is visible\n\n### Fixed\n\n- **Context-gauge % wrong for Opus 4.7 (1M) and other new models**: The dashboard\'s context gauge was dividing by 200K for Claude Opus 4.7 (native 1M), inflating the displayed %. The shared model \u2192 context-window map now includes Opus/Sonnet 4.7 (1M), GPT-5.4 (1.05M), GPT-5.3-Codex (400K), and GPT-5.3-Codex-Spark (128K). Claude Code\'s `[1m]` suffix is now also honored as an explicit 1M marker\n- **Silent Sonnet-priced fallback for unknown models**: Codex, GPT-5.x, and o-series rows were being rendered at Sonnet rates. Unknown-model rows now render as `\u2014` in yellow instead of inventing a dollar figure\n\n### Changed\n\n- **`historical-data.json` schema v2**: reads `priced` flag and `unpricedModelIds` from records written by the latest VS Code extension; v1 records still read correctly\n\n## [0.17.1] - 2026-04-13\n\n### Fixed\n\n- **Codex multi-home session discovery**: Provider detection now scans all candidate Codex home directories, fixing missed sessions when the managed profile home is empty but the system `~/.codex/` has activity\n\n## [0.17.0] - 2026-04-13\n\n### Added\n\n- **Multi-provider account management**: `sidekick account` now supports `--provider codex` for Codex profile management alongside Claude Code accounts\n- **Codex account lifecycle**: `--add` prepares a profile and spawns `codex login`; `--switch-to` and `--remove` accept email, label, or profile ID\n- **Quota snapshot fallback**: `sidekick quota` for Codex shows cached rate-limit snapshots when no active session exists, with "cached from" timestamp\n\n### Fixed\n\n- **Email normalization**: Claude account lookup normalizes email case for reliable matching\n\n## [0.16.1] - 2026-03-27\n\n### Fixed\n\n- **Dashboard provider status scoping**: The TUI now shows degraded-service notices only for the monitored provider \u2014 Claude for Claude Code sessions, OpenAI for Codex sessions, and no status banner for OpenCode\n\n## [0.16.0] - 2026-03-23\n\n### Changed\n\n- **Consistent cost formatting**: All cost displays (`stats`, `context`, Sessions panel, narrative prompt) now use shared `formatCost()` with intelligent decimal precision (4 places for < $0.01, 2 otherwise)\n- **QuotaService**: Rewritten to wrap shared `QuotaPoller` with exponential backoff instead of manual polling loop\n- **modelContext**: Now re-exports `getModelInfo` from shared library alongside `getContextWindowSize`\n\n## [0.15.2] - 2026-03-18\n\n### Fixed\n\n- **CLI help descriptions**: Updated `quota` and `status` command descriptions to reflect provider-aware behavior\n- **`sidekick quota --provider`**: Added local `--provider` option so `sidekick quota --provider codex` works naturally\n\n## [0.15.0] - 2026-03-18\n\n### Added\n\n- **OpenAI status page monitoring**: CLI dashboard now shows OpenAI API status alongside Claude API status\n- **Codex rate limits in dashboard**: Sessions panel displays Codex rate-limit data with "Rate Limits" header instead of "Quota"\n- **Provider-aware `sidekick quota` command**: Detects active provider and shows Codex rate limits, Claude subscription quota, or an informational message for OpenCode\n\n### Fixed\n\n- **QuotaService polling for Codex**: Dashboard no longer starts Claude OAuth quota polling when the active provider is Codex\n\n## [0.14.2] - 2026-03-16\n\n### Fixed\n\n- **Quota polling interval**: Reduced quota refresh from every 30 seconds to every 5 minutes to avoid unnecessary API calls\n- **SessionsPanel `detailWidth()` call**: Removed unused parameter from `detailWidth()` in the Sessions panel quota rendering\n\n## [0.14.1] - 2026-03-14\n\n### Fixed\n\n- **Per-model context window sizes**: Dashboard context gauge now shows correct utilization for Claude Opus 4.6 (1M context) and other models with non-200K windows\n\n### Changed\n\n- **Shared model context lookup**: CLI dashboard now uses the centralized `getModelContextWindowSize()` from `sidekick-shared` instead of a local duplicate map\n\n## [0.14.0] - 2026-03-12\n\n### Added\n\n- **`sidekick account` Command**: Manage Claude Code accounts from the terminal \u2014 list saved accounts, add the current account with an optional label, switch to the next or a specific account, and remove accounts. Supports `--json` output for scripting\n- **Quota Account Label**: `sidekick quota` now shows the active account email and label above the quota bars when multi-account is enabled\n- **macOS Keychain Support**: `sidekick account` and `sidekick quota` now read and write credentials via the system Keychain on macOS, fixing account switching and quota checks on Mac\n\n## [0.13.8] - 2026-03-12\n\n### Changed\n\n- **Structured quota failure output**: `sidekick quota` now renders consistent auth, rate-limit, server, network, and unexpected-failure copy from shared quota failure descriptors while preserving `--json` machine-readable output\n- **Dashboard unavailable quota rendering**: The Sessions panel now shows Claude Code quota failures inline instead of hiding the quota section whenever subscription data is unavailable\n- **Quota transition toasts**: The Ink dashboard now fires low-noise toast notifications only when Claude Code quota failure state changes, avoiding repeated alerts every polling interval\n\n## [0.13.7] - 2026-03-11\n\n### Changed\n\n- **npm README sync**: Updated the published CLI package README to reflect current OpenCode monitoring behavior, platform-specific data directories, and the `sqlite3` runtime requirement\n- **README badge cleanup**: Removed the Ask DeepWiki badge from the published CLI package README; the repo root README still keeps it\n\n## [0.13.6] - 2026-03-11\n\n### Changed\n\n- **Refreshed CLI Dashboard Wordmark**: Updated the dashboard wordmark/header styling for a cleaner splash and dashboard identity\n\n### Fixed\n\n- **OpenCode dashboard startup**: OpenCode DB-backed session discovery now resolves projects by worktree, sandboxes, and session directory instead of quietly behaving like no session exists\n- **OpenCode runtime notices**: The CLI now prints an OpenCode-only actionable notice when `opencode.db` exists but `sqlite3` is missing, blocked, or otherwise unusable in the current shell environment\n\n## [0.13.5] - 2026-03-10\n\n### Added\n\n- **`sidekick status` Command**: One-shot Claude API status check with color-coded text output and `--json` mode\n- **Dashboard Status Banner**: Status bar shows a colored `\u25CF API minor/major/critical` indicator when Claude is degraded; Sessions panel Summary tab shows an "API Status" section with affected components and active incident details. Polls every 60s\n\n## [0.13.4] - 2026-03-08\n\n### Fixed\n\n- **Onboarding Phrase Spam**: Splash screen and detail pane motivational phrases memoized \u2014 no longer flicker every render tick (fixes [#13](https://github.com/cesarandreslopez/sidekick-agent-hub/issues/13))\n\n### Changed\n\n- **Simplified Logo**: Replaced 6-line ASCII robot art with compact text header in splash, help, and changelog overlays\n- **Removed Dead Code**: Removed unused `getSplashContent()` and `HELP_HEADER` exports from branding module\n\n## [0.13.3] - 2026-03-04\n\n_No CLI-specific changes in this release._\n\n## [0.13.2] - 2026-03-04\n\n_No CLI-specific changes in this release._\n\n## [0.13.1] - 2026-03-04\n\n### Added\n\n- **`sidekick quota` Command**: One-shot subscription quota check showing 5-hour and 7-day utilization with color-coded progress bars and reset countdowns \u2014 supports `--json` for machine-readable output\n- **Quota Projections**: Elapsed-time projections shown in `sidekick quota` output and TUI dashboard quota section \u2014 displays projected end-of-window utilization next to current value (e.g., `40% \u2192 100%`), included in `--json` output as `projectedFiveHour` / `projectedSevenDay`\n\n## [0.13.0] - 2026-03-03\n\n_No CLI-specific changes in this release._\n\n## [0.12.10] - 2026-03-01\n\n### Added\n\n- **Events Panel** (key 7): Scrollable live event stream with colored type badges (`[USR]`, `[AST]`, `[TOOL]`, `[RES]`), timestamps, and keyword-highlighted summaries; detail tabs for full event JSON and surrounding context\n- **Charts Panel** (key 8): Tool frequency horizontal bars, event type distribution, 60-minute activity heatmap using `\u2591\u2592\u2593\u2588` intensity characters, and pattern analysis with frequency bars and template text\n- **Multi-Mode Filter**: `/` filter overlay now supports four modes \u2014 substring, fuzzy, regex, and date range \u2014 Tab cycles modes, regex mode shows red validation errors\n- **Search Term Highlighting**: Active filter terms highlighted in blue within side list items\n- **Timeline Keyword Coloring**: Event summaries in the Sessions panel Timeline tab now use semantic keyword coloring \u2014 errors red, success green, tool names cyan, file paths magenta\n\n### Removed\n\n- **Search Panel**: Removed redundant Search panel (previously key 7) \u2014 the `/` filter with multi-mode support serves the same purpose\n\n## [0.12.9] - 2026-02-28\n\n### Added\n\n- **Standalone Data Commands**: `sidekick tasks`, `sidekick decisions`, `sidekick notes`, `sidekick stats`, `sidekick handoff` for accessing project data without launching the TUI\n- **`sidekick search <query>`**: Cross-session full-text search from the terminal\n- **`sidekick context`**: Composite output of tasks, decisions, notes, and handoff for piping into other tools\n- **`--list` flag on `sidekick dump`**: Discover available session IDs before requiring `--session <id>`\n- **Search Panel**: Search panel (panel 7) wired into the TUI dashboard\n\n### Changed\n\n- **`taskMerger` utility**: Duplicate `mergeTasks` logic extracted into shared `taskMerger` utility\n- **Model constants**: Hardcoded model IDs extracted to named constants\n\n### Fixed\n\n- **`convention` icon**: Notes panel icon replaced with valid `tip` type\n- **Linux clipboard**: Now supports Wayland (`wl-copy`) and `xsel` fallbacks, with error messages instead of silent failure\n- **`provider.dispose()`**: Added to `dump` and `report` commands (prevents SQLite connection leaks)\n\n## [0.12.8] - 2026-02-28\n\n### Changed\n\n- **Dashboard UI/UX Polish**: Visual overhaul for better hierarchy, consistency, and readability\n - Splash screen and help overlay now display the robot ASCII logo\n - Toast notifications show severity icons (\u2718 error, \u26A0 warning, \u25CF info) with inner padding\n - Focused pane uses double-border for clear focus indication\n - Section dividers (`\u2500\u2500 Title \u2500\u2500\u2500\u2500`) replace bare bold headers in summary, agents, and context attribution\n - Tab bar: active tab underlined in magenta, inactive tabs dimmed, bracket syntax removed\n - Status bar: segmented layout with `\u2502` separators; keys bold, labels dim\n - Summary metrics condensed: elapsed/events/compactions on one line, tokens on one line with cache rate and cost\n - Sparklines display peak metadata annotations\n - Progress bars use blessed color tags for consistent coloring\n - Help overlay uses dot-leader alignment for all keybinding rows\n - Empty state hints per panel (e.g. "Tasks appear as your agent works.")\n - Session picker groups sessions by provider with section headers when multiple providers are present\n\n## [0.12.7] - 2026-02-27\n\n### Added\n\n- **HTML Session Report**: `sidekick report` command generates a self-contained HTML report and opens it in the default browser\n - Options: `--session`, `--output`, `--theme` (dark/light), `--no-open`, `--no-thinking`\n - TUI Dashboard: press `r` to generate and open an HTML report for the current session\n\n## [0.12.6] - 2026-02-26\n\n### Added\n\n- **Session Dump Command**: `sidekick dump` exports session data in text, markdown, or JSON format with `--format`, `--width`, and `--expand` options\n- **Plans Panel Re-enabled**: Plans panel restored in CLI dashboard with plan file discovery from `~/.claude/plans/`\n- **Enhanced Status Bar**: Session info display improved with richer metadata\n\n### Fixed\n\n- **Old snapshot format migration**: Restoring pre-0.12.3 session snapshots no longer shows empty timeline entries\n\n### Changed\n\n- **Phrase library moved to shared**: CLI-specific phrase formatting kept local, all phrase content now from `sidekick-shared`\n\n## [0.12.5] - 2026-02-24\n\n### Fixed\n\n- **Update check too slow to notice new versions**: Reduced npm registry cache TTL from 24 hours to 4 hours so upgrade notices appear sooner after a new release\n\n## [0.12.4] - 2026-02-24\n\n### Fixed\n\n- **Session crash on upgrade**: Fixed `d.timestamp.getTime is not a function` error when restoring tool call data from session snapshots \u2014 `Date` objects were serialized to strings by JSON but not rehydrated on restore, causing the session monitor to crash on first run after upgrading from 0.12.2 to 0.12.3\n\n## [0.12.3] - 2026-02-24\n\n### Added\n\n- **Latest-node indicator**: The most recently added node in tree and boxed mind map views is now marked with a yellow indicator\n- **Plan analytics in mind map**: Tree and boxed views now display plan progress and per-step metrics\n - Tree view: plan header shows completion stats; steps show complexity, duration, tokens, tool calls, and errors in metadata brackets\n - Box view: progress bar with completion percentage; steps show right-aligned metrics; subtitle shows step count and total duration\n- **Cross-provider plan extraction**: Shared `PlanExtractor` now handles Claude Code (EnterPlanMode/ExitPlanMode) and OpenCode (`<proposed_plan>` XML) plans \u2014 previously only Codex plans were shown\n- **Enriched plan data model**: Plan steps include duration, token count, tool call count, and error messages\n- **Phase-grouped plan display**: When a plan has phase structure, tree and boxed views group steps under phase headers with context lines from the original plan markdown\n- **Node type filter**: Press `f` on the Mind Map tab to cycle through node type filters (file, tool, task, subagent, command, plan, knowledge-note) \u2014 non-matching sections render dimmed in grey\n\n### Fixed\n\n- **Kanban board regression**: Subagent and plan-step tasks now correctly appear in the kanban board\n\n### Changed\n\n- **Plans panel temporarily disabled**: The Plans panel in the CLI dashboard is disabled until plan-mode event capture is reliably working end-to-end. Plan nodes in the mind map remain active.\n- `DashboardState` now delegates to shared `EventAggregator` instead of maintaining its own aggregation logic\n\n## [0.12.2] - 2026-02-23\n\n### Added\n\n- **Update notifications**: The dashboard now checks the npm registry for newer versions on startup and shows a yellow banner in the status bar when an update is available (e.g., `v0.13.0 available \u2014 npm i -g sidekick-agent-hub`). Results are cached for 24 hours to avoid repeated network requests.\n\n## [0.12.1] - 2026-02-23\n\n### Fixed\n\n- **VS Code integration**: Fixed exit code 127 when the extension launches the CLI dashboard on systems using nvm or volta (node binary not found when shell init is bypassed)\n\n## [0.12.0] - 2026-02-22\n\n### Added\n\n- **"Open CLI Dashboard" VS Code Integration**: New VS Code command `Sidekick: Open CLI Dashboard` launches the TUI dashboard in an integrated terminal\n - Install the CLI with `npm install -g sidekick-agent-hub`\n\n## [0.11.0] - 2026-02-19\n\n### Added\n\n- **Initial Release**: Full-screen TUI dashboard for monitoring agent sessions from the terminal\n - Ink-based terminal UI with panels for sessions, tasks, kanban, mind map, notes, decisions, search, files, and git diff\n - Multi-provider support: auto-detects Claude Code, OpenCode, and Codex sessions\n - Reads from `~/.config/sidekick/` \u2014 the same data files the VS Code extension writes\n - Usage: `sidekick dashboard [--project <path>] [--provider <id>]`\n';
80604
81131
  }
80605
81132
  });
80606
81133
 
@@ -82689,6 +83216,11 @@ function formatCountdown(minutes) {
82689
83216
  function printPeakHoursBlock(state) {
82690
83217
  process.stdout.write(source_default.bold("Claude Peak Hours\n"));
82691
83218
  process.stdout.write(source_default.dim("\u2500".repeat(50) + "\n"));
83219
+ if (state.notApplicable) {
83220
+ process.stdout.write(source_default.dim(` ${state.note || "Claude peak hours do not apply to this provider."}
83221
+ `));
83222
+ return;
83223
+ }
82692
83224
  if (state.unavailable) {
82693
83225
  process.stdout.write(source_default.dim(" Peak-hours status unavailable (promoclock.co unreachable).\n"));
82694
83226
  return;
@@ -82919,6 +83451,183 @@ var init_quota = __esm({
82919
83451
  }
82920
83452
  });
82921
83453
 
83454
+ // src/commands/quotaHistory.ts
83455
+ var quotaHistory_exports = {};
83456
+ __export(quotaHistory_exports, {
83457
+ bucketForUtilization: () => bucketForUtilization,
83458
+ colorForBucket: () => colorForBucket,
83459
+ quotaHistoryAction: () => quotaHistoryAction,
83460
+ renderProviderHeatmap: () => renderProviderHeatmap
83461
+ });
83462
+ function bucketForUtilization(util) {
83463
+ if (util <= 0) return 0;
83464
+ if (util < 25) return 1;
83465
+ if (util < 50) return 2;
83466
+ if (util < 75) return 3;
83467
+ return 4;
83468
+ }
83469
+ function colorForBucket(bucket) {
83470
+ switch (bucket) {
83471
+ case 0:
83472
+ return source_default.dim;
83473
+ case 1:
83474
+ return source_default.green;
83475
+ case 2:
83476
+ return source_default.yellow;
83477
+ case 3:
83478
+ return source_default.hex("#ff8800");
83479
+ case 4:
83480
+ return source_default.red.bold;
83481
+ default:
83482
+ return source_default.white;
83483
+ }
83484
+ }
83485
+ function bucketsToCells(buckets) {
83486
+ return buckets.map((b) => ({
83487
+ date: b.date,
83488
+ utilization: Math.max(b.maxUtilizationFiveHour, b.maxUtilizationSevenDay),
83489
+ unavailable: b.anyUnavailable,
83490
+ samples: b.samples
83491
+ }));
83492
+ }
83493
+ function renderProviderHeatmap(label, cells, weeks) {
83494
+ const cols = weeks;
83495
+ const rows = 7;
83496
+ const totalCells = cols * rows;
83497
+ let firstDayOfWeek = 0;
83498
+ if (cells.length > 0) {
83499
+ firstDayOfWeek = (/* @__PURE__ */ new Date(`${cells[0].date}T00:00:00Z`)).getUTCDay();
83500
+ }
83501
+ const padded = [];
83502
+ for (let i = 0; i < firstDayOfWeek; i += 1) padded.push(null);
83503
+ for (const cell of cells) padded.push(cell);
83504
+ while (padded.length < totalCells) padded.push(null);
83505
+ while (padded.length > totalCells) padded.shift();
83506
+ const lines = [];
83507
+ for (let row = 0; row < rows; row += 1) {
83508
+ const dayLabel = source_default.dim(DAY_LABELS[row].padEnd(4));
83509
+ const glyphs = [];
83510
+ for (let col = 0; col < cols; col += 1) {
83511
+ const cell = padded[col * rows + row];
83512
+ if (!cell) {
83513
+ glyphs.push(source_default.dim(" "));
83514
+ continue;
83515
+ }
83516
+ if (cell.unavailable && cell.samples > 0) {
83517
+ glyphs.push(source_default.red("\xD7"));
83518
+ continue;
83519
+ }
83520
+ const bucket = bucketForUtilization(cell.utilization);
83521
+ glyphs.push(colorForBucket(bucket)(BUCKET_GLYPHS[bucket]));
83522
+ }
83523
+ lines.push(`${dayLabel}${glyphs.join("")}`);
83524
+ }
83525
+ let peak = 0;
83526
+ let sum = 0;
83527
+ let sampledDays = 0;
83528
+ let unavailableDays = 0;
83529
+ let totalSamples = 0;
83530
+ for (const cell of cells) {
83531
+ if (cell.samples === 0) continue;
83532
+ sampledDays += 1;
83533
+ totalSamples += cell.samples;
83534
+ if (cell.utilization > peak) peak = cell.utilization;
83535
+ sum += cell.utilization;
83536
+ if (cell.unavailable) unavailableDays += 1;
83537
+ }
83538
+ const avg = sampledDays > 0 ? Math.round(sum / sampledDays) : 0;
83539
+ const peakColor = colorForBucket(bucketForUtilization(peak));
83540
+ const avgColor = colorForBucket(bucketForUtilization(avg));
83541
+ const header = source_default.bold(label) + source_default.dim(` \xB7 ${weeks} weeks \xB7 ${sampledDays} day(s) with samples`);
83542
+ const footer = [
83543
+ `Peak ${peakColor(`${peak}%`)}`,
83544
+ `Avg ${avgColor(`${avg}%`)}`,
83545
+ unavailableDays > 0 ? source_default.red(`Unavailable ${unavailableDays} day(s)`) : null,
83546
+ source_default.dim(`Samples ${totalSamples.toLocaleString()}`)
83547
+ ].filter(Boolean).join(" \xB7 ");
83548
+ return [header, ...lines, footer].join("\n");
83549
+ }
83550
+ function legend() {
83551
+ const swatches = BUCKET_GLYPHS.map((g, i) => colorForBucket(i)(g)).join("");
83552
+ return source_default.dim("Less ") + swatches + source_default.dim(" More");
83553
+ }
83554
+ function parseProviderFilter(value) {
83555
+ if (typeof value !== "string") return "auto";
83556
+ const lower = value.toLowerCase();
83557
+ if (lower === "claude" || lower === "claude-code") return "claude";
83558
+ if (lower === "codex") return "codex";
83559
+ return "auto";
83560
+ }
83561
+ function clampWeeks(value) {
83562
+ const parsed = typeof value === "string" ? Number.parseInt(value, 10) : typeof value === "number" ? value : 13;
83563
+ if (!Number.isFinite(parsed)) return 13;
83564
+ return Math.max(1, Math.min(26, parsed));
83565
+ }
83566
+ async function quotaHistoryAction(_localOpts, cmd) {
83567
+ const globalOpts = cmd.parent?.parent?.opts() ?? cmd.parent?.opts() ?? {};
83568
+ const localOpts = cmd.opts();
83569
+ const jsonOutput = !!globalOpts.json;
83570
+ const weeks = clampWeeks(localOpts.weeks);
83571
+ const providerFilter = parseProviderFilter(localOpts.provider ?? globalOpts.provider);
83572
+ const workspacePath = typeof localOpts.workspace === "string" && localOpts.workspace ? localOpts.workspace : process.cwd();
83573
+ const workspaceId = (0, import_sidekick_shared30.getWorkspaceIdFromPath)(workspacePath);
83574
+ const toMs = Date.now();
83575
+ const fromMs = toMs - (weeks * 7 - 1) * MS_PER_DAY;
83576
+ const from = new Date(fromMs).toISOString();
83577
+ const to = new Date(toMs).toISOString();
83578
+ const wanted = providerFilter === "auto" ? ["claude", "codex"] : [providerFilter];
83579
+ const providerCells = {};
83580
+ for (const provider of wanted) {
83581
+ const buckets = await (0, import_sidekick_shared30.readQuotaHistoryDailyBuckets)({ workspaceId, provider, from, to });
83582
+ providerCells[provider] = bucketsToCells(buckets);
83583
+ }
83584
+ const payload = {
83585
+ workspaceId,
83586
+ weeks,
83587
+ providers: {
83588
+ ...providerCells.claude ? { claude: { cells: providerCells.claude } } : {},
83589
+ ...providerCells.codex ? { codex: { cells: providerCells.codex } } : {}
83590
+ },
83591
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
83592
+ };
83593
+ if (jsonOutput) {
83594
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
83595
+ return;
83596
+ }
83597
+ const sections = [];
83598
+ const claudeHasData = (payload.providers.claude?.cells ?? []).some((c) => c.samples > 0);
83599
+ const codexHasData = (payload.providers.codex?.cells ?? []).some((c) => c.samples > 0);
83600
+ if (providerFilter === "auto" && !claudeHasData && !codexHasData) {
83601
+ process.stdout.write(
83602
+ source_default.yellow(`No quota history yet for workspace ${source_default.bold(workspaceId)}.`) + "\n" + source_default.dim(`Run a Claude Max or Codex session in this workspace, or override with --workspace <path>.`) + "\n"
83603
+ );
83604
+ return;
83605
+ }
83606
+ if (payload.providers.claude && (providerFilter !== "auto" || claudeHasData)) {
83607
+ sections.push(renderProviderHeatmap("Claude", payload.providers.claude.cells, weeks));
83608
+ }
83609
+ if (payload.providers.codex && (providerFilter !== "auto" || codexHasData)) {
83610
+ sections.push(renderProviderHeatmap("Codex", payload.providers.codex.cells, weeks));
83611
+ }
83612
+ const header = source_default.dim(`workspace ${workspaceId} \xB7 ${from.slice(0, 10)} \u2192 ${to.slice(0, 10)}`);
83613
+ process.stdout.write(`${header}
83614
+ ${legend()}
83615
+
83616
+ ${sections.join("\n\n")}
83617
+ `);
83618
+ }
83619
+ var import_sidekick_shared30, MS_PER_DAY, BUCKET_GLYPHS, DAY_LABELS;
83620
+ var init_quotaHistory = __esm({
83621
+ "src/commands/quotaHistory.ts"() {
83622
+ "use strict";
83623
+ init_source();
83624
+ import_sidekick_shared30 = __toESM(require_dist(), 1);
83625
+ MS_PER_DAY = 864e5;
83626
+ BUCKET_GLYPHS = ["\xB7", "\u2591", "\u2592", "\u2593", "\u2588"];
83627
+ DAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
83628
+ }
83629
+ });
83630
+
82922
83631
  // src/commands/status.ts
82923
83632
  var status_exports = {};
82924
83633
  __export(status_exports, {
@@ -82965,9 +83674,9 @@ async function statusAction(_opts, cmd) {
82965
83674
  const providerId = resolveProviderId(globalOpts);
82966
83675
  const wantsPeak = providerId === "claude-code";
82967
83676
  const [claude, openai, peak] = await Promise.all([
82968
- (0, import_sidekick_shared30.fetchProviderStatus)(),
82969
- (0, import_sidekick_shared30.fetchOpenAIStatus)(),
82970
- wantsPeak ? (0, import_sidekick_shared30.fetchPeakHoursStatus)() : Promise.resolve(null)
83677
+ (0, import_sidekick_shared31.fetchProviderStatus)(),
83678
+ (0, import_sidekick_shared31.fetchOpenAIStatus)(),
83679
+ wantsPeak ? (0, import_sidekick_shared31.fetchPeakHoursStatus)() : Promise.resolve(null)
82971
83680
  ]);
82972
83681
  if (jsonOutput) {
82973
83682
  process.stdout.write(JSON.stringify({ claude, openai, peak }, null, 2) + "\n");
@@ -82981,12 +83690,12 @@ async function statusAction(_opts, cmd) {
82981
83690
  printPeakHoursBlock(peak);
82982
83691
  }
82983
83692
  }
82984
- var import_sidekick_shared30;
83693
+ var import_sidekick_shared31;
82985
83694
  var init_status = __esm({
82986
83695
  "src/commands/status.ts"() {
82987
83696
  "use strict";
82988
83697
  init_source();
82989
- import_sidekick_shared30 = __toESM(require_dist(), 1);
83698
+ import_sidekick_shared31 = __toESM(require_dist(), 1);
82990
83699
  init_peakHoursRender();
82991
83700
  init_cli();
82992
83701
  }
@@ -82999,20 +83708,24 @@ __export(peak_exports, {
82999
83708
  });
83000
83709
  async function peakAction(_opts, cmd) {
83001
83710
  const globalOpts = cmd.parent.opts();
83711
+ const localOpts = cmd.opts();
83002
83712
  const jsonOutput = !!globalOpts.json;
83003
- const state = await (0, import_sidekick_shared31.fetchPeakHoursStatus)();
83713
+ const providerOpts = localOpts.provider ? { provider: localOpts.provider } : globalOpts;
83714
+ const providerId = resolveProviderId(providerOpts);
83715
+ const state = (0, import_sidekick_shared32.isClaudeCodeSessionProvider)(providerId) ? await (0, import_sidekick_shared32.fetchPeakHoursStatus)() : (0, import_sidekick_shared32.createPeakHoursNotApplicableState)(providerId);
83004
83716
  if (jsonOutput) {
83005
83717
  process.stdout.write(JSON.stringify(state, null, 2) + "\n");
83006
83718
  return;
83007
83719
  }
83008
83720
  printPeakHoursBlock(state);
83009
83721
  }
83010
- var import_sidekick_shared31;
83722
+ var import_sidekick_shared32;
83011
83723
  var init_peak = __esm({
83012
83724
  "src/commands/peak.ts"() {
83013
83725
  "use strict";
83014
- import_sidekick_shared31 = __toESM(require_dist(), 1);
83726
+ import_sidekick_shared32 = __toESM(require_dist(), 1);
83015
83727
  init_peakHoursRender();
83728
+ init_cli();
83016
83729
  }
83017
83730
  });
83018
83731
 
@@ -83043,13 +83756,13 @@ async function accountAction(_opts, cmd) {
83043
83756
  }
83044
83757
  function claudeAccountAction(opts, jsonOutput) {
83045
83758
  if (opts.add) {
83046
- const result = (0, import_sidekick_shared32.addCurrentAccount)(opts.label);
83759
+ const result = (0, import_sidekick_shared33.addCurrentAccount)(opts.label);
83047
83760
  if (!result.success) {
83048
83761
  process.stderr.write(source_default.red(result.error ?? "Failed to save account.") + "\n");
83049
83762
  process.exit(1);
83050
83763
  return;
83051
83764
  }
83052
- const active2 = (0, import_sidekick_shared32.readActiveClaudeAccount)();
83765
+ const active2 = (0, import_sidekick_shared33.readActiveClaudeAccount)();
83053
83766
  if (jsonOutput) {
83054
83767
  process.stdout.write(JSON.stringify({ action: "added", provider: "claude-code", email: active2?.email, label: opts.label }) + "\n");
83055
83768
  } else {
@@ -83058,14 +83771,14 @@ function claudeAccountAction(opts, jsonOutput) {
83058
83771
  return;
83059
83772
  }
83060
83773
  if (opts.remove) {
83061
- const accounts2 = (0, import_sidekick_shared32.listAccounts)();
83774
+ const accounts2 = (0, import_sidekick_shared33.listAccounts)();
83062
83775
  const target = findClaudeAccount(opts.remove, accounts2);
83063
83776
  if (!target) {
83064
83777
  process.stderr.write(source_default.red(`Account "${opts.remove}" not found.`) + "\n");
83065
83778
  process.exit(1);
83066
83779
  return;
83067
83780
  }
83068
- const result = (0, import_sidekick_shared32.removeAccount)(target.uuid);
83781
+ const result = (0, import_sidekick_shared33.removeAccount)(target.uuid);
83069
83782
  if (!result.success) {
83070
83783
  process.stderr.write(source_default.red(result.error ?? "Failed to remove account.") + "\n");
83071
83784
  process.exit(1);
@@ -83079,14 +83792,14 @@ function claudeAccountAction(opts, jsonOutput) {
83079
83792
  return;
83080
83793
  }
83081
83794
  if (opts.switchTo) {
83082
- const accounts2 = (0, import_sidekick_shared32.listAccounts)();
83795
+ const accounts2 = (0, import_sidekick_shared33.listAccounts)();
83083
83796
  const target = findClaudeAccount(opts.switchTo, accounts2);
83084
83797
  if (!target) {
83085
83798
  process.stderr.write(source_default.red(`Account "${opts.switchTo}" not found.`) + "\n");
83086
83799
  process.exit(1);
83087
83800
  return;
83088
83801
  }
83089
- const result = (0, import_sidekick_shared32.switchToAccount)(target.uuid);
83802
+ const result = (0, import_sidekick_shared33.switchToAccount)(target.uuid);
83090
83803
  if (!result.success) {
83091
83804
  process.stderr.write(source_default.red(result.error ?? "Failed to switch.") + "\n");
83092
83805
  process.exit(1);
@@ -83100,17 +83813,17 @@ function claudeAccountAction(opts, jsonOutput) {
83100
83813
  return;
83101
83814
  }
83102
83815
  if (opts.switch) {
83103
- const accounts2 = (0, import_sidekick_shared32.listAccounts)();
83816
+ const accounts2 = (0, import_sidekick_shared33.listAccounts)();
83104
83817
  if (accounts2.length < 2) {
83105
83818
  process.stderr.write(source_default.yellow("Need at least 2 saved accounts to switch.") + "\n");
83106
83819
  process.exit(1);
83107
83820
  return;
83108
83821
  }
83109
- const active2 = (0, import_sidekick_shared32.getActiveAccount)();
83822
+ const active2 = (0, import_sidekick_shared33.getActiveAccount)();
83110
83823
  const currentIdx = active2 ? accounts2.findIndex((a) => a.uuid === active2.uuid) : -1;
83111
83824
  const nextIdx = (currentIdx + 1) % accounts2.length;
83112
83825
  const target = accounts2[nextIdx];
83113
- const result = (0, import_sidekick_shared32.switchToAccount)(target.uuid);
83826
+ const result = (0, import_sidekick_shared33.switchToAccount)(target.uuid);
83114
83827
  if (!result.success) {
83115
83828
  process.stderr.write(source_default.red(result.error ?? "Failed to switch.") + "\n");
83116
83829
  process.exit(1);
@@ -83123,9 +83836,9 @@ function claudeAccountAction(opts, jsonOutput) {
83123
83836
  }
83124
83837
  return;
83125
83838
  }
83126
- const accounts = (0, import_sidekick_shared32.listAccounts)();
83839
+ const accounts = (0, import_sidekick_shared33.listAccounts)();
83127
83840
  if (accounts.length === 0) {
83128
- const current = (0, import_sidekick_shared32.readActiveClaudeAccount)();
83841
+ const current = (0, import_sidekick_shared33.readActiveClaudeAccount)();
83129
83842
  if (jsonOutput) {
83130
83843
  process.stdout.write(JSON.stringify({ provider: "claude-code", accounts: [], current: current ?? null }) + "\n");
83131
83844
  } else if (current) {
@@ -83137,14 +83850,14 @@ function claudeAccountAction(opts, jsonOutput) {
83137
83850
  return;
83138
83851
  }
83139
83852
  if (jsonOutput) {
83140
- const active2 = (0, import_sidekick_shared32.getActiveAccount)();
83853
+ const active2 = (0, import_sidekick_shared33.getActiveAccount)();
83141
83854
  process.stdout.write(JSON.stringify({
83142
83855
  provider: "claude-code",
83143
83856
  accounts: accounts.map((a) => ({ ...a, active: a.uuid === active2?.uuid }))
83144
83857
  }, null, 2) + "\n");
83145
83858
  return;
83146
83859
  }
83147
- const active = (0, import_sidekick_shared32.getActiveAccount)();
83860
+ const active = (0, import_sidekick_shared33.getActiveAccount)();
83148
83861
  process.stdout.write(source_default.bold("Claude Accounts\n"));
83149
83862
  process.stdout.write(source_default.dim("\u2500".repeat(50) + "\n"));
83150
83863
  for (const account of accounts) {
@@ -83191,7 +83904,7 @@ function codexAccountAction(opts, jsonOutput) {
83191
83904
  process.exit(1);
83192
83905
  return;
83193
83906
  }
83194
- const prepared = (0, import_sidekick_shared32.prepareCodexAccount)(opts.label);
83907
+ const prepared = (0, import_sidekick_shared33.prepareCodexAccount)(opts.label);
83195
83908
  if (!prepared.success) {
83196
83909
  process.stderr.write(source_default.red(prepared.error ?? "Failed to prepare Codex account.") + "\n");
83197
83910
  process.exit(1);
@@ -83209,14 +83922,14 @@ function codexAccountAction(opts, jsonOutput) {
83209
83922
  process.exit(1);
83210
83923
  return;
83211
83924
  }
83212
- const finalized = (0, import_sidekick_shared32.finalizeCodexAccount)(prepared.profileId);
83925
+ const finalized = (0, import_sidekick_shared33.finalizeCodexAccount)(prepared.profileId);
83213
83926
  if (!finalized.success) {
83214
83927
  process.stderr.write(source_default.red(finalized.error ?? "Failed to finalize Codex account.") + "\n");
83215
83928
  process.exit(1);
83216
83929
  return;
83217
83930
  }
83218
83931
  }
83219
- const active2 = (0, import_sidekick_shared32.getActiveCodexAccount)();
83932
+ const active2 = (0, import_sidekick_shared33.getActiveCodexAccount)();
83220
83933
  if (jsonOutput) {
83221
83934
  process.stdout.write(JSON.stringify({
83222
83935
  action: "added",
@@ -83232,14 +83945,14 @@ function codexAccountAction(opts, jsonOutput) {
83232
83945
  return;
83233
83946
  }
83234
83947
  if (opts.remove) {
83235
- const accounts2 = (0, import_sidekick_shared32.listCodexAccounts)();
83948
+ const accounts2 = (0, import_sidekick_shared33.listCodexAccounts)();
83236
83949
  const target = findCodexAccount(opts.remove, accounts2);
83237
83950
  if (!target) {
83238
83951
  process.stderr.write(source_default.red(`Codex account "${opts.remove}" not found.`) + "\n");
83239
83952
  process.exit(1);
83240
83953
  return;
83241
83954
  }
83242
- const result = (0, import_sidekick_shared32.removeCodexAccount)(target.id);
83955
+ const result = (0, import_sidekick_shared33.removeCodexAccount)(target.id);
83243
83956
  if (!result.success) {
83244
83957
  process.stderr.write(source_default.red(result.error ?? "Failed to remove Codex account.") + "\n");
83245
83958
  process.exit(1);
@@ -83253,14 +83966,14 @@ function codexAccountAction(opts, jsonOutput) {
83253
83966
  return;
83254
83967
  }
83255
83968
  if (opts.switchTo) {
83256
- const accounts2 = (0, import_sidekick_shared32.listCodexAccounts)();
83969
+ const accounts2 = (0, import_sidekick_shared33.listCodexAccounts)();
83257
83970
  const target = findCodexAccount(opts.switchTo, accounts2);
83258
83971
  if (!target) {
83259
83972
  process.stderr.write(source_default.red(`Codex account "${opts.switchTo}" not found.`) + "\n");
83260
83973
  process.exit(1);
83261
83974
  return;
83262
83975
  }
83263
- const result = (0, import_sidekick_shared32.switchToCodexAccount)(target.id);
83976
+ const result = (0, import_sidekick_shared33.switchToCodexAccount)(target.id);
83264
83977
  if (!result.success) {
83265
83978
  process.stderr.write(source_default.red(result.error ?? "Failed to switch Codex account.") + "\n");
83266
83979
  process.exit(1);
@@ -83274,17 +83987,17 @@ function codexAccountAction(opts, jsonOutput) {
83274
83987
  return;
83275
83988
  }
83276
83989
  if (opts.switch) {
83277
- const accounts2 = (0, import_sidekick_shared32.listCodexAccounts)();
83990
+ const accounts2 = (0, import_sidekick_shared33.listCodexAccounts)();
83278
83991
  if (accounts2.length < 2) {
83279
83992
  process.stderr.write(source_default.yellow("Need at least 2 saved Codex accounts to switch.") + "\n");
83280
83993
  process.exit(1);
83281
83994
  return;
83282
83995
  }
83283
- const active2 = (0, import_sidekick_shared32.getActiveCodexAccount)();
83996
+ const active2 = (0, import_sidekick_shared33.getActiveCodexAccount)();
83284
83997
  const currentIdx = active2 ? accounts2.findIndex((account) => account.id === active2.id) : -1;
83285
83998
  const nextIdx = (currentIdx + 1) % accounts2.length;
83286
83999
  const target = accounts2[nextIdx];
83287
- const result = (0, import_sidekick_shared32.switchToCodexAccount)(target.id);
84000
+ const result = (0, import_sidekick_shared33.switchToCodexAccount)(target.id);
83288
84001
  if (!result.success) {
83289
84002
  process.stderr.write(source_default.red(result.error ?? "Failed to switch Codex account.") + "\n");
83290
84003
  process.exit(1);
@@ -83297,7 +84010,7 @@ function codexAccountAction(opts, jsonOutput) {
83297
84010
  }
83298
84011
  return;
83299
84012
  }
83300
- const accounts = (0, import_sidekick_shared32.listCodexAccounts)();
84013
+ const accounts = (0, import_sidekick_shared33.listCodexAccounts)();
83301
84014
  if (accounts.length === 0) {
83302
84015
  if (jsonOutput) {
83303
84016
  process.stdout.write(JSON.stringify({ provider: "codex", accounts: [], current: null }) + "\n");
@@ -83307,7 +84020,7 @@ function codexAccountAction(opts, jsonOutput) {
83307
84020
  return;
83308
84021
  }
83309
84022
  if (jsonOutput) {
83310
- const active2 = (0, import_sidekick_shared32.getActiveCodexAccount)();
84023
+ const active2 = (0, import_sidekick_shared33.getActiveCodexAccount)();
83311
84024
  process.stdout.write(JSON.stringify({
83312
84025
  provider: "codex",
83313
84026
  accounts: accounts.map((account) => ({
@@ -83317,7 +84030,7 @@ function codexAccountAction(opts, jsonOutput) {
83317
84030
  }, null, 2) + "\n");
83318
84031
  return;
83319
84032
  }
83320
- const active = (0, import_sidekick_shared32.getActiveCodexAccount)();
84033
+ const active = (0, import_sidekick_shared33.getActiveCodexAccount)();
83321
84034
  process.stdout.write(source_default.bold("Codex Accounts\n"));
83322
84035
  process.stdout.write(source_default.dim("\u2500".repeat(50) + "\n"));
83323
84036
  for (const account of accounts) {
@@ -83328,12 +84041,12 @@ function codexAccountAction(opts, jsonOutput) {
83328
84041
  `);
83329
84042
  }
83330
84043
  }
83331
- var import_sidekick_shared32;
84044
+ var import_sidekick_shared33;
83332
84045
  var init_account = __esm({
83333
84046
  "src/commands/account.ts"() {
83334
84047
  "use strict";
83335
84048
  init_source();
83336
- import_sidekick_shared32 = __toESM(require_dist(), 1);
84049
+ import_sidekick_shared33 = __toESM(require_dist(), 1);
83337
84050
  init_cli();
83338
84051
  }
83339
84052
  });
@@ -83348,12 +84061,12 @@ async function handoffAction(_opts, cmd) {
83348
84061
  const workspacePath = globalOpts.project || process.cwd();
83349
84062
  const jsonOutput = !!globalOpts.json;
83350
84063
  try {
83351
- const rawSlug = (0, import_sidekick_shared33.getProjectSlugRaw)(workspacePath);
83352
- const resolvedSlug = (0, import_sidekick_shared33.getProjectSlug)(workspacePath);
84064
+ const rawSlug = (0, import_sidekick_shared34.getProjectSlugRaw)(workspacePath);
84065
+ const resolvedSlug = (0, import_sidekick_shared34.getProjectSlug)(workspacePath);
83353
84066
  const slugs = rawSlug !== resolvedSlug ? [rawSlug, resolvedSlug] : [rawSlug];
83354
84067
  let content = null;
83355
84068
  for (const slug of slugs) {
83356
- content = await (0, import_sidekick_shared33.readLatestHandoff)(slug);
84069
+ content = await (0, import_sidekick_shared34.readLatestHandoff)(slug);
83357
84070
  if (content) break;
83358
84071
  }
83359
84072
  if (!content) {
@@ -83378,12 +84091,12 @@ async function handoffAction(_opts, cmd) {
83378
84091
  process.exit(1);
83379
84092
  }
83380
84093
  }
83381
- var import_sidekick_shared33;
84094
+ var import_sidekick_shared34;
83382
84095
  var init_handoff = __esm({
83383
84096
  "src/commands/handoff.ts"() {
83384
84097
  "use strict";
83385
84098
  init_source();
83386
- import_sidekick_shared33 = __toESM(require_dist(), 1);
84099
+ import_sidekick_shared34 = __toESM(require_dist(), 1);
83387
84100
  }
83388
84101
  });
83389
84102
 
@@ -83397,35 +84110,35 @@ function resolveProviderId(opts, defaultProvider = "auto") {
83397
84110
  if (defaultProvider !== "auto") {
83398
84111
  return defaultProvider;
83399
84112
  }
83400
- return (0, import_sidekick_shared34.detectProvider)();
84113
+ return (0, import_sidekick_shared35.detectProvider)();
83401
84114
  }
83402
84115
  function resolveProvider(opts) {
83403
84116
  const id = resolveProviderId(opts);
83404
84117
  switch (id) {
83405
84118
  case "opencode":
83406
- return new import_sidekick_shared35.OpenCodeProvider();
84119
+ return new import_sidekick_shared36.OpenCodeProvider();
83407
84120
  case "codex":
83408
- return new import_sidekick_shared35.CodexProvider();
84121
+ return new import_sidekick_shared36.CodexProvider();
83409
84122
  case "claude-code":
83410
84123
  default:
83411
- return new import_sidekick_shared35.ClaudeCodeProvider();
84124
+ return new import_sidekick_shared36.ClaudeCodeProvider();
83412
84125
  }
83413
84126
  }
83414
- var import_sidekick_shared34, import_node, import_sidekick_shared35, defaultAccountsReady, program2, dashCmd, dumpCmd, ctxCmd, reportCmd, searchCmd, tasksCmd, decisionsCmd, notesCmd, statsCmd, quotaCmd, statusCmd, peakCmd, accountCmd, handoffCmd;
84127
+ var import_sidekick_shared35, import_node, import_sidekick_shared36, defaultAccountsReady, program2, dashCmd, dumpCmd, ctxCmd, reportCmd, searchCmd, tasksCmd, decisionsCmd, notesCmd, statsCmd, quotaCmd, statusCmd, peakCmd, accountCmd, handoffCmd;
83415
84128
  var init_cli = __esm({
83416
84129
  "src/cli.ts"() {
83417
84130
  init_esm();
83418
- import_sidekick_shared34 = __toESM(require_dist(), 1);
83419
- import_node = __toESM(require_node(), 1);
83420
84131
  import_sidekick_shared35 = __toESM(require_dist(), 1);
84132
+ import_node = __toESM(require_node(), 1);
84133
+ import_sidekick_shared36 = __toESM(require_dist(), 1);
83421
84134
  (0, import_node.hydratePricingCatalog)({
83422
84135
  cacheDir: path7.join(os5.homedir(), ".config", "sidekick")
83423
84136
  }).catch(() => {
83424
84137
  });
83425
- defaultAccountsReady = (0, import_sidekick_shared34.ensureDefaultAccounts)().catch(() => {
84138
+ defaultAccountsReady = (0, import_sidekick_shared35.ensureDefaultAccounts)().catch(() => {
83426
84139
  });
83427
84140
  program2 = new Command();
83428
- program2.name("sidekick").description("Query Sidekick project intelligence from the command line").version("0.18.2").option("--json", "Output as JSON").option("--project <path>", "Override project path (default: cwd)").option("--provider <id>", "Provider: claude-code, opencode, codex, auto (default: auto)");
84141
+ program2.name("sidekick").description("Query Sidekick project intelligence from the command line").version("0.18.4").option("--json", "Output as JSON").option("--project <path>", "Override project path (default: cwd)").option("--provider <id>", "Provider: claude-code, opencode, codex, auto (default: auto)");
83429
84142
  program2.hook("preAction", async () => {
83430
84143
  await defaultAccountsReady;
83431
84144
  });
@@ -83479,13 +84192,17 @@ var init_cli = __esm({
83479
84192
  const { quotaAction: quotaAction2 } = await Promise.resolve().then(() => (init_quota(), quota_exports));
83480
84193
  return quotaAction2(_opts, cmd);
83481
84194
  });
84195
+ quotaCmd.command("history").description("Render a 13-week heatmap of quota utilization for the current workspace").option("--weeks <n>", "Weeks of history to render (default: 13, clamped 1-26)", "13").option("--provider <id>", "Limit to a single runtime provider: claude or codex (default: both)").option("--workspace <path>", "Workspace path used to derive the history scope (default: cwd)").action(async (_opts, cmd) => {
84196
+ const { quotaHistoryAction: quotaHistoryAction2 } = await Promise.resolve().then(() => (init_quotaHistory(), quotaHistory_exports));
84197
+ return quotaHistoryAction2(_opts, cmd);
84198
+ });
83482
84199
  program2.addCommand(quotaCmd);
83483
84200
  statusCmd = new Command("status").description("Show API status (Claude and OpenAI)").action(async (_opts, cmd) => {
83484
84201
  const { statusAction: statusAction2 } = await Promise.resolve().then(() => (init_status(), status_exports));
83485
84202
  return statusAction2(_opts, cmd);
83486
84203
  });
83487
84204
  program2.addCommand(statusCmd);
83488
- peakCmd = new Command("peak").description("Show whether Claude is currently in peak hours (faster session-limit drain)").action(async (_opts, cmd) => {
84205
+ peakCmd = new Command("peak").description("Show whether Claude is currently in peak hours (faster session-limit drain)").option("--provider <id>", "Provider: claude-code, opencode, codex, auto (default: auto)").action(async (_opts, cmd) => {
83489
84206
  const { peakAction: peakAction2 } = await Promise.resolve().then(() => (init_peak(), peak_exports));
83490
84207
  return peakAction2(_opts, cmd);
83491
84208
  });