sidekick-shared 0.21.1 → 0.21.3

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.
Files changed (77) hide show
  1. package/README.md +45 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.js +18 -14
  4. package/dist/accountStatus.js +4 -1
  5. package/dist/accounts.js +60 -25
  6. package/dist/aggregation/EventAggregator.d.ts +14 -11
  7. package/dist/aggregation/EventAggregator.js +68 -31
  8. package/dist/aggregation/PatternExtractor.js +5 -4
  9. package/dist/autoSwitch.js +6 -12
  10. package/dist/claudeProfiles.js +3 -3
  11. package/dist/codexProfiles.js +45 -22
  12. package/dist/codexQuota.js +44 -16
  13. package/dist/codexQuotaWatcher.js +10 -3
  14. package/dist/context/composer.js +8 -6
  15. package/dist/context/sessionContext.js +12 -9
  16. package/dist/credentialIO.js +10 -8
  17. package/dist/extractors/sessionAssets.js +9 -1
  18. package/dist/extractors/sources/claudeAssets.js +7 -1
  19. package/dist/extractors/sources/codexAssets.js +14 -3
  20. package/dist/extractors/toolCall.js +1 -4
  21. package/dist/formatters/eventHighlighter.js +148 -28
  22. package/dist/formatters/noiseClassifier.js +3 -11
  23. package/dist/formatters/sessionDump.js +22 -13
  24. package/dist/formatters/toolSummary.js +1 -3
  25. package/dist/index.d.ts +25 -23
  26. package/dist/index.js +12 -3
  27. package/dist/modelContext.js +10 -8
  28. package/dist/modelInfo.js +11 -6
  29. package/dist/multiProviderQuotaService.js +10 -7
  30. package/dist/parsers/codexParser.js +170 -103
  31. package/dist/parsers/debugLogParser.js +5 -4
  32. package/dist/parsers/openCodeParser.js +66 -48
  33. package/dist/parsers/planExtractor.js +3 -3
  34. package/dist/parsers/sessionActivityDetector.js +10 -6
  35. package/dist/parsers/sessionPathResolver.js +29 -34
  36. package/dist/parsers/subagentScanner.js +21 -20
  37. package/dist/parsers/subagentTraceParser.js +7 -4
  38. package/dist/phrases.js +11 -11
  39. package/dist/providerStatus.js +3 -4
  40. package/dist/providers/claudeCode.js +3 -1
  41. package/dist/providers/codex.js +35 -14
  42. package/dist/providers/codexDatabase.js +14 -5
  43. package/dist/providers/detect.js +10 -6
  44. package/dist/providers/openCode.d.ts +4 -4
  45. package/dist/providers/openCode.js +96 -68
  46. package/dist/providers/openCodeDatabase.js +44 -17
  47. package/dist/quota.d.ts +19 -0
  48. package/dist/quota.js +50 -8
  49. package/dist/quotaHistory.js +10 -3
  50. package/dist/quotaSnapshots.d.ts +1 -1
  51. package/dist/quotaSnapshots.js +2 -2
  52. package/dist/readers/decisions.js +1 -1
  53. package/dist/readers/notes.js +5 -3
  54. package/dist/readers/plans.js +43 -23
  55. package/dist/readers/tasks.js +1 -1
  56. package/dist/report/htmlHelpers.js +135 -16
  57. package/dist/report/htmlReportGenerator.js +38 -12
  58. package/dist/report/transcriptParser.js +8 -2
  59. package/dist/schemas/index.d.ts +4 -4
  60. package/dist/schemas/quota.js +2 -10
  61. package/dist/schemas/sessionEvent.js +20 -10
  62. package/dist/search/advancedFilter.js +3 -3
  63. package/dist/search/sessionSearch.js +10 -4
  64. package/dist/terminalSync.js +4 -2
  65. package/dist/turns/assistantTurn.js +5 -1
  66. package/dist/watchers/eventBridge.js +56 -26
  67. package/dist/watchers/factory.js +5 -2
  68. package/dist/watchers/jsonlTail.js +3 -1
  69. package/dist/watchers/jsonlWatcher.js +128 -52
  70. package/dist/watchers/providerReaderWatcher.js +3 -1
  71. package/dist/watchers/sqliteWatcher.js +52 -22
  72. package/dist/zaiQuota.d.ts +6 -6
  73. package/dist/zaiQuota.js +6 -10
  74. package/dist/zaiQuotaApi.d.ts +33 -0
  75. package/dist/zaiQuotaApi.js +279 -0
  76. package/dist/zaiQuotaWatcher.js +10 -9
  77. package/package.json +5 -2
@@ -63,18 +63,30 @@ function normalizeMessage(msg) {
63
63
  const model = data.modelID || undefined;
64
64
  if (role === 'user') {
65
65
  events.push({
66
- providerId: 'opencode', type: 'user', timestamp: ts,
67
- summary: content || '(user message)', model, raw: data,
66
+ providerId: 'opencode',
67
+ type: 'user',
68
+ timestamp: ts,
69
+ summary: content || '(user message)',
70
+ model,
71
+ raw: data,
68
72
  });
69
73
  }
70
74
  else if (role === 'assistant') {
71
75
  events.push({
72
- providerId: 'opencode', type: 'assistant', timestamp: ts,
73
- summary: content || '(thinking...)', tokens, cost, model, raw: data,
76
+ providerId: 'opencode',
77
+ type: 'assistant',
78
+ timestamp: ts,
79
+ summary: content || '(thinking...)',
80
+ tokens,
81
+ cost,
82
+ model,
83
+ raw: data,
74
84
  });
75
85
  }
76
86
  }
77
- catch { /* skip malformed */ }
87
+ catch {
88
+ /* skip malformed */
89
+ }
78
90
  return events;
79
91
  }
80
92
  function normalizePart(part) {
@@ -93,13 +105,15 @@ function normalizePart(part) {
93
105
  const toolInput = state?.input;
94
106
  const input = data.args ? truncate(JSON.stringify(data.args), 80) : '';
95
107
  // Include input in raw so PlanExtractor can access raw.input
96
- const toolRaw = eventType === 'tool_use'
97
- ? { ...data, input: toolInput || {} }
98
- : data;
108
+ const toolRaw = eventType === 'tool_use' ? { ...data, input: toolInput || {} } : data;
99
109
  events.push({
100
- providerId: 'opencode', type: eventType, timestamp: ts,
110
+ providerId: 'opencode',
111
+ type: eventType,
112
+ timestamp: ts,
101
113
  summary: input ? `${toolName} ${input}` : toolName,
102
- toolName, toolInput: input || undefined, raw: toolRaw,
114
+ toolName,
115
+ toolInput: input || undefined,
116
+ raw: toolRaw,
103
117
  });
104
118
  }
105
119
  else if (type === 'text') {
@@ -107,7 +121,9 @@ function normalizePart(part) {
107
121
  if (text) {
108
122
  // Wrap raw to include message.content for <proposed_plan> extraction
109
123
  events.push({
110
- providerId: 'opencode', type: 'assistant', timestamp: ts,
124
+ providerId: 'opencode',
125
+ type: 'assistant',
126
+ timestamp: ts,
111
127
  summary: truncate(text, 200),
112
128
  raw: { ...data, message: { content: [{ type: 'text', text }] } },
113
129
  });
@@ -116,7 +132,9 @@ function normalizePart(part) {
116
132
  if (direction) {
117
133
  const syntheticToolName = direction === 'enter' ? 'EnterPlanMode' : 'ExitPlanMode';
118
134
  events.push({
119
- providerId: 'opencode', type: 'tool_use', timestamp: ts,
135
+ providerId: 'opencode',
136
+ type: 'tool_use',
137
+ timestamp: ts,
120
138
  summary: syntheticToolName,
121
139
  toolName: syntheticToolName,
122
140
  raw: { input: { source: 'opencode_text_heuristic' } },
@@ -125,7 +143,9 @@ function normalizePart(part) {
125
143
  }
126
144
  }
127
145
  }
128
- catch { /* skip malformed */ }
146
+ catch {
147
+ /* skip malformed */
148
+ }
129
149
  return events;
130
150
  }
131
151
  function extractContent(data) {
@@ -158,7 +178,9 @@ class SqliteSessionWatcher {
158
178
  this.callbacks = callbacks;
159
179
  this.db = new openCodeDatabase_1.OpenCodeDatabase(path.dirname(dbPath));
160
180
  }
161
- get isActive() { return this._isActive; }
181
+ get isActive() {
182
+ return this._isActive;
183
+ }
162
184
  /** Seek to a timestamp position. Must be called before start(). */
163
185
  seekTo(position) {
164
186
  this.lastMessageTime = position;
@@ -186,8 +208,12 @@ class SqliteSessionWatcher {
186
208
  this.skipToEnd();
187
209
  }
188
210
  // Watch db and wal files for changes
189
- this.watchFile(this.dbPath, (w) => { this.fsWatcher = w; });
190
- this.watchFile(this.dbPath + '-wal', (w) => { this.walWatcher = w; });
211
+ this.watchFile(this.dbPath, (w) => {
212
+ this.fsWatcher = w;
213
+ });
214
+ this.watchFile(this.dbPath + '-wal', (w) => {
215
+ this.walWatcher = w;
216
+ });
191
217
  // Fallback polling
192
218
  this.pollTimer = setInterval(() => {
193
219
  if (this._isActive)
@@ -223,10 +249,14 @@ class SqliteSessionWatcher {
223
249
  const watcher = fs.watch(filePath, { persistent: false }, () => {
224
250
  this.debouncedPoll();
225
251
  });
226
- watcher.on('error', () => { });
252
+ watcher.on('error', () => {
253
+ /* ignore watch errors */
254
+ });
227
255
  setter(watcher);
228
256
  }
229
- catch { /* fs.watch not available */ }
257
+ catch {
258
+ /* fs.watch not available */
259
+ }
230
260
  }
231
261
  debouncedPoll() {
232
262
  if (this.debounceTimer)
@@ -239,10 +269,10 @@ class SqliteSessionWatcher {
239
269
  const messages = this.db.getMessagesForSession(this.sessionId);
240
270
  const parts = this.db.getPartsForSession(this.sessionId);
241
271
  if (messages.length > 0) {
242
- this.lastMessageTime = Math.max(...messages.map(m => m.time_created));
272
+ this.lastMessageTime = Math.max(...messages.map((m) => m.time_created));
243
273
  }
244
274
  if (parts.length > 0) {
245
- this.lastPartTime = Math.max(...parts.map(p => p.time_created));
275
+ this.lastPartTime = Math.max(...parts.map((p) => p.time_created));
246
276
  }
247
277
  }
248
278
  pollNewData() {
@@ -252,7 +282,7 @@ class SqliteSessionWatcher {
252
282
  const messages = this.db.getMessagesForSession(this.sessionId);
253
283
  const parts = this.db.getPartsForSession(this.sessionId);
254
284
  // Emit new messages
255
- const newMessages = messages.filter(m => m.time_created > this.lastMessageTime);
285
+ const newMessages = messages.filter((m) => m.time_created > this.lastMessageTime);
256
286
  for (const msg of newMessages) {
257
287
  const events = normalizeMessage(msg);
258
288
  for (const e of events)
@@ -261,7 +291,7 @@ class SqliteSessionWatcher {
261
291
  this.lastMessageTime = msg.time_created;
262
292
  }
263
293
  // Emit new parts
264
- const newParts = parts.filter(p => p.time_created > this.lastPartTime);
294
+ const newParts = parts.filter((p) => p.time_created > this.lastPartTime);
265
295
  for (const part of newParts) {
266
296
  const events = normalizePart(part);
267
297
  for (const e of events)
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Stateless quota accumulator for z.ai coding plans.
2
+ * Deprecated stateless quota accumulator for z.ai coding plans.
3
3
  *
4
- * z.ai (GLM Coding Plan) does not expose an HTTP quota/usage API (verified
5
- * against the canonical OpenAPI spec at https://docs.z.ai/openapi.json).
6
- * The only programmatic signals are:
4
+ * This estimator is kept for compatibility with consumers that imported the
5
+ * old helpers directly. Product code should use `zaiQuotaApi.ts`, which calls
6
+ * z.ai's authoritative quota endpoint. The legacy estimator only uses:
7
7
  *
8
8
  * 1. Per-call token `usage` returned inline on each chat completion —
9
9
  * which OpenCode persists into `opencode.db` message rows tagged with
@@ -13,8 +13,8 @@
13
13
  * in the human-readable message text.
14
14
  *
15
15
  * This module is therefore a **derivation** rather than a fetcher: callers
16
- * feed it observed assistant turns (and any error events), and it computes
17
- * an estimated `QuotaState` keyed against the published per-tier budgets.
16
+ * feed it observed assistant turns (and any error events), and it computes a
17
+ * compatibility `QuotaState` keyed against the published per-tier budgets.
18
18
  *
19
19
  * Polling / eventing / persistence are the caller's responsibility —
20
20
  * see `ZaiQuotaWatcher` for the bundled watcher.
package/dist/zaiQuota.js CHANGED
@@ -132,18 +132,14 @@ function resolveZaiTier(configured, accumulated) {
132
132
  function inferZaiQuotaState(accumulated, tier, options = {}) {
133
133
  const budget = exports.ZAI_TIER_BUDGETS[tier];
134
134
  const capturedAt = options.capturedAt ?? new Date().toISOString();
135
- const fiveHourUtil = budget.fiveHour > 0
136
- ? Math.min((accumulated.fiveHourPrompts / budget.fiveHour) * 100, 200)
137
- : 0;
138
- const weeklyUtil = budget.weekly > 0
139
- ? Math.min((accumulated.weeklyPrompts / budget.weekly) * 100, 200)
140
- : 0;
141
- const fiveHourResetsAt = options.authoritativeFiveHourResetAt
142
- ?? (accumulated.fiveHourStartedAtMs !== null
135
+ const fiveHourUtil = budget.fiveHour > 0 ? Math.min((accumulated.fiveHourPrompts / budget.fiveHour) * 100, 200) : 0;
136
+ const weeklyUtil = budget.weekly > 0 ? Math.min((accumulated.weeklyPrompts / budget.weekly) * 100, 200) : 0;
137
+ const fiveHourResetsAt = options.authoritativeFiveHourResetAt ??
138
+ (accumulated.fiveHourStartedAtMs !== null
143
139
  ? new Date(accumulated.fiveHourStartedAtMs + FIVE_HOUR_MS).toISOString()
144
140
  : '');
145
- const sevenDayResetsAt = options.authoritativeWeeklyResetAt
146
- ?? (accumulated.weeklyStartedAtMs !== null
141
+ const sevenDayResetsAt = options.authoritativeWeeklyResetAt ??
142
+ (accumulated.weeklyStartedAtMs !== null
147
143
  ? new Date(accumulated.weeklyStartedAtMs + SEVEN_DAY_MS).toISOString()
148
144
  : '');
149
145
  const fiveHour = {
@@ -0,0 +1,33 @@
1
+ import type { QuotaState } from './quota';
2
+ import type { ProviderQuotaState } from './providerQuota';
3
+ type ZaiSnapshotProvider = 'zai';
4
+ type SnapshotReader = (providerId: ZaiSnapshotProvider, accountId: string) => QuotaState | null;
5
+ type SnapshotWriter = (providerId: ZaiSnapshotProvider, accountId: string, quota: QuotaState) => void;
6
+ export type ZaiPlatform = 'ZAI' | 'ZHIPU';
7
+ export type ZaiCredentialSource = 'opencode' | 'env';
8
+ export interface ZaiCredentials {
9
+ authToken: string;
10
+ baseUrl: string;
11
+ platform: ZaiPlatform;
12
+ source: ZaiCredentialSource;
13
+ }
14
+ export interface ReadZaiCredentialsOptions {
15
+ openCodeDataDir?: string;
16
+ env?: Record<string, string | undefined>;
17
+ }
18
+ export interface ZaiQuotaApiOptions extends ReadZaiCredentialsOptions {
19
+ credentials?: ZaiCredentials | null;
20
+ fetchImpl?: typeof fetch;
21
+ capturedAt?: string;
22
+ timeoutMs?: number;
23
+ }
24
+ export interface ZaiQuotaResolveOptions extends ZaiQuotaApiOptions {
25
+ accountId?: string;
26
+ readSnapshot?: SnapshotReader;
27
+ writeSnapshot?: SnapshotWriter;
28
+ }
29
+ export declare function readZaiCredentials(options?: ReadZaiCredentialsOptions): ZaiCredentials | null;
30
+ export declare function quotaStateFromZaiQuotaLimitPayload(payload: unknown, capturedAt?: string): QuotaState;
31
+ export declare function fetchZaiQuotaFromApi(options?: ZaiQuotaApiOptions): Promise<QuotaState>;
32
+ export declare function resolveZaiQuota(options?: ZaiQuotaResolveOptions): Promise<ProviderQuotaState<'zai'>>;
33
+ export {};
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readZaiCredentials = readZaiCredentials;
7
+ exports.quotaStateFromZaiQuotaLimitPayload = quotaStateFromZaiQuotaLimitPayload;
8
+ exports.fetchZaiQuotaFromApi = fetchZaiQuotaFromApi;
9
+ exports.resolveZaiQuota = resolveZaiQuota;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const quotaSnapshots_1 = require("./quotaSnapshots");
14
+ const quota_1 = require("./quota");
15
+ const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/anthropic';
16
+ const QUOTA_PATH = '/api/monitor/usage/quota/limit';
17
+ const DEFAULT_ACCOUNT_ID = 'default';
18
+ const DEFAULT_TIMEOUT_MS = 10_000;
19
+ function unavailableZaiQuotaState(error, meta = {}, capturedAt = new Date().toISOString()) {
20
+ return {
21
+ fiveHour: { utilization: 0, resetsAt: '' },
22
+ sevenDay: { utilization: 0, resetsAt: '' },
23
+ available: false,
24
+ error,
25
+ providerId: 'zai',
26
+ source: 'api',
27
+ capturedAt,
28
+ fiveHourLabel: '5-Hour',
29
+ sevenDayLabel: 'Weekly',
30
+ ...meta,
31
+ };
32
+ }
33
+ function openCodeDataDirCandidates() {
34
+ const candidates = [];
35
+ const xdg = process.env.XDG_DATA_HOME;
36
+ if (xdg)
37
+ candidates.push(path_1.default.join(xdg, 'opencode'));
38
+ candidates.push(path_1.default.join(os_1.default.homedir(), '.local', 'share', 'opencode'));
39
+ if (process.platform === 'darwin') {
40
+ candidates.push(path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'opencode'));
41
+ }
42
+ else if (process.platform === 'win32') {
43
+ candidates.push(path_1.default.join(process.env.LOCALAPPDATA ||
44
+ process.env.APPDATA ||
45
+ path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'opencode'));
46
+ }
47
+ return Array.from(new Set(candidates));
48
+ }
49
+ function readOpenCodeAuthToken(openCodeDataDir) {
50
+ const candidates = openCodeDataDir ? [openCodeDataDir] : openCodeDataDirCandidates();
51
+ for (const dataDir of candidates) {
52
+ try {
53
+ const parsed = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dataDir, 'auth.json'), 'utf8'));
54
+ const codingPlan = parsed['zai-coding-plan'];
55
+ const zai = parsed.zai;
56
+ if (typeof codingPlan?.key === 'string' && codingPlan.key.trim())
57
+ return codingPlan.key.trim();
58
+ if (typeof zai?.key === 'string' && zai.key.trim())
59
+ return zai.key.trim();
60
+ }
61
+ catch {
62
+ // Try the next candidate.
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function platformFromBaseUrl(baseUrl) {
68
+ if (baseUrl.includes('api.z.ai'))
69
+ return 'ZAI';
70
+ if (baseUrl.includes('open.bigmodel.cn') || baseUrl.includes('dev.bigmodel.cn'))
71
+ return 'ZHIPU';
72
+ return null;
73
+ }
74
+ function readZaiCredentials(options = {}) {
75
+ const openCodeToken = readOpenCodeAuthToken(options.openCodeDataDir);
76
+ if (openCodeToken) {
77
+ return {
78
+ authToken: openCodeToken,
79
+ baseUrl: DEFAULT_ZAI_BASE_URL,
80
+ platform: 'ZAI',
81
+ source: 'opencode',
82
+ };
83
+ }
84
+ const env = options.env ?? process.env;
85
+ const authToken = env.ANTHROPIC_AUTH_TOKEN?.trim();
86
+ const baseUrl = env.ANTHROPIC_BASE_URL?.trim();
87
+ if (!authToken || !baseUrl)
88
+ return null;
89
+ const platform = platformFromBaseUrl(baseUrl);
90
+ if (!platform)
91
+ return null;
92
+ return {
93
+ authToken,
94
+ baseUrl,
95
+ platform,
96
+ source: 'env',
97
+ };
98
+ }
99
+ function quotaLimitUrl(baseUrl) {
100
+ try {
101
+ const parsed = new URL(baseUrl);
102
+ return `${parsed.protocol}//${parsed.host}${QUOTA_PATH}`;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function tokenLimitEntries(payload) {
109
+ const limits = payload.data?.limits ?? payload.limits ?? [];
110
+ return limits.filter((item) => item?.type === 'TOKENS_LIMIT' && typeof item.percentage === 'number');
111
+ }
112
+ function isFiveHourLimit(item) {
113
+ return item.type === 'TOKENS_LIMIT' && item.unit === 3 && item.number === 5;
114
+ }
115
+ function isWeeklyLimit(item) {
116
+ return item.type === 'TOKENS_LIMIT' && item.unit === 6 && item.number === 1;
117
+ }
118
+ function isoFromEpochMs(value) {
119
+ if (value == null)
120
+ return '';
121
+ const ms = typeof value === 'number' ? value : Number(value);
122
+ if (!Number.isFinite(ms) || ms <= 0)
123
+ return '';
124
+ return new Date(ms).toISOString();
125
+ }
126
+ function displayPlanName(level) {
127
+ if (!level)
128
+ return 'z.ai Coding Plan';
129
+ return `z.ai ${level.charAt(0).toUpperCase()}${level.slice(1)}`;
130
+ }
131
+ function quotaStateFromZaiQuotaLimitPayload(payload, capturedAt = new Date().toISOString()) {
132
+ const data = payload;
133
+ const tokenLimits = tokenLimitEntries(data);
134
+ const fiveHourLimit = tokenLimits.find(isFiveHourLimit) ?? tokenLimits[0];
135
+ const weeklyLimit = tokenLimits.find(isWeeklyLimit) ?? tokenLimits.find((item) => item !== fiveHourLimit);
136
+ const level = data.data?.level;
137
+ if (!fiveHourLimit || !weeklyLimit) {
138
+ return unavailableZaiQuotaState('z.ai quota API returned no token quota windows.', {
139
+ failureKind: 'unknown',
140
+ }, capturedAt);
141
+ }
142
+ return (0, quota_1.withQuotaProjections)({
143
+ fiveHour: {
144
+ utilization: fiveHourLimit.percentage ?? 0,
145
+ resetsAt: isoFromEpochMs(fiveHourLimit.nextResetTime),
146
+ },
147
+ sevenDay: {
148
+ utilization: weeklyLimit.percentage ?? 0,
149
+ resetsAt: isoFromEpochMs(weeklyLimit.nextResetTime),
150
+ },
151
+ available: true,
152
+ providerId: 'zai',
153
+ source: 'api',
154
+ capturedAt,
155
+ fiveHourLabel: '5-Hour',
156
+ sevenDayLabel: 'Weekly',
157
+ planType: level,
158
+ limitId: level ? `zai-${level}` : 'zai-coding-plan',
159
+ limitName: displayPlanName(level),
160
+ }, {
161
+ fiveHourWindowMs: quota_1.FIVE_HOUR_WINDOW_MS,
162
+ sevenDayWindowMs: quota_1.SEVEN_DAY_WINDOW_MS,
163
+ capturedAt,
164
+ });
165
+ }
166
+ function parseRetryAfterMs(retryAfter) {
167
+ if (!retryAfter)
168
+ return undefined;
169
+ const seconds = Number(retryAfter);
170
+ if (Number.isFinite(seconds) && seconds >= 0)
171
+ return seconds * 1000;
172
+ const dateMs = Date.parse(retryAfter);
173
+ if (Number.isFinite(dateMs))
174
+ return Math.max(0, dateMs - Date.now());
175
+ return undefined;
176
+ }
177
+ function failureKindForStatus(status) {
178
+ if (status === 401 || status === 403)
179
+ return 'auth';
180
+ if (status === 429)
181
+ return 'rate_limit';
182
+ if (status >= 500)
183
+ return 'server';
184
+ return 'unknown';
185
+ }
186
+ function sanitizeErrorMessage(message, authToken) {
187
+ let result = message;
188
+ if (authToken) {
189
+ result = result.split(authToken).join('<redacted>');
190
+ }
191
+ return result.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, 'Bearer <redacted>');
192
+ }
193
+ async function fetchZaiQuotaFromApi(options = {}) {
194
+ const capturedAt = options.capturedAt ?? new Date().toISOString();
195
+ const credentials = options.credentials ?? readZaiCredentials(options);
196
+ if (!credentials) {
197
+ return unavailableZaiQuotaState('No z.ai credentials found. Sign in to OpenCode with z.ai or set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN.', { failureKind: 'auth' }, capturedAt);
198
+ }
199
+ const url = quotaLimitUrl(credentials.baseUrl);
200
+ if (!url) {
201
+ return unavailableZaiQuotaState('Invalid z.ai base URL.', { failureKind: 'unknown' }, capturedAt);
202
+ }
203
+ const fetchImpl = options.fetchImpl ?? fetch;
204
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
205
+ const controller = new AbortController();
206
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
207
+ timeout.unref?.();
208
+ try {
209
+ const response = await fetchImpl(url, {
210
+ method: 'GET',
211
+ headers: {
212
+ Authorization: credentials.authToken,
213
+ 'Accept-Language': 'en-US,en',
214
+ Accept: 'application/json',
215
+ 'Content-Type': 'application/json',
216
+ },
217
+ signal: controller.signal,
218
+ });
219
+ const body = await response.text();
220
+ let parsed;
221
+ try {
222
+ parsed = JSON.parse(body);
223
+ }
224
+ catch {
225
+ parsed = null;
226
+ }
227
+ if (!response.ok) {
228
+ const failureKind = failureKindForStatus(response.status);
229
+ const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'));
230
+ const detail = parsed && typeof parsed === 'object' && 'msg' in parsed
231
+ ? String(parsed.msg ?? '')
232
+ : '';
233
+ const baseMessage = failureKind === 'auth'
234
+ ? `z.ai quota API rejected credentials (HTTP ${response.status}).`
235
+ : `z.ai quota API error (HTTP ${response.status}).`;
236
+ return unavailableZaiQuotaState(sanitizeErrorMessage([baseMessage, detail].filter(Boolean).join(' '), credentials.authToken), { failureKind, httpStatus: response.status, retryAfterMs }, capturedAt);
237
+ }
238
+ return quotaStateFromZaiQuotaLimitPayload(parsed, capturedAt);
239
+ }
240
+ catch (error) {
241
+ const isAbort = error instanceof Error && error.name === 'AbortError';
242
+ return unavailableZaiQuotaState(isAbort ? 'z.ai quota API timed out.' : 'z.ai quota API network error.', { failureKind: 'network' }, capturedAt);
243
+ }
244
+ finally {
245
+ clearTimeout(timeout);
246
+ }
247
+ }
248
+ function enrichZaiQuota(state) {
249
+ const withProjections = (0, quota_1.withQuotaProjections)(state);
250
+ return {
251
+ ...withProjections,
252
+ runtimeProvider: 'zai',
253
+ providerId: 'zai',
254
+ fiveHourLabel: state.fiveHourLabel ?? '5-Hour',
255
+ sevenDayLabel: state.sevenDayLabel ?? 'Weekly',
256
+ };
257
+ }
258
+ async function resolveZaiQuota(options = {}) {
259
+ const accountId = options.accountId ?? DEFAULT_ACCOUNT_ID;
260
+ const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
261
+ const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
262
+ const apiQuota = await fetchZaiQuotaFromApi(options);
263
+ if (apiQuota.available) {
264
+ writeSnapshot('zai', accountId, apiQuota);
265
+ return enrichZaiQuota(apiQuota);
266
+ }
267
+ const cached = readSnapshot('zai', accountId);
268
+ if (cached) {
269
+ return enrichZaiQuota({
270
+ ...cached,
271
+ providerId: 'zai',
272
+ source: 'cache',
273
+ stale: true,
274
+ fiveHourLabel: cached.fiveHourLabel ?? '5-Hour',
275
+ sevenDayLabel: cached.sevenDayLabel ?? 'Weekly',
276
+ });
277
+ }
278
+ return enrichZaiQuota(apiQuota);
279
+ }
@@ -2,15 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZaiQuotaWatcher = void 0;
4
4
  /**
5
- * Event-driven watcher that derives a z.ai coding-plan `QuotaState` from
6
- * observed OpenCode assistant turns.
5
+ * Deprecated event-driven watcher that derives a z.ai coding-plan
6
+ * `QuotaState` from observed OpenCode assistant turns.
7
7
  *
8
- * z.ai does not expose a quota API, so this watcher maintains rolling
9
- * 5-hour / 7-day windows in memory and recomputes an estimated `QuotaState`
10
- * whenever a new turn arrives. The consumer is responsible for feeding it
11
- * z.ai-routed assistant turns (and any trapped error events) — typically
12
- * by subscribing to `SessionMonitor.onSessionEvent` and filtering on
13
- * `providerID ∈ {zai, zai-coding-plan}`.
8
+ * This watcher is retained for compatibility. Product code now uses
9
+ * `zaiQuotaApi.ts` to call z.ai's authoritative quota endpoint. The legacy
10
+ * watcher maintains rolling 5-hour / 7-day windows in memory and recomputes a
11
+ * derived `QuotaState` whenever a new turn arrives. The consumer is
12
+ * responsible for feeding it z.ai-routed assistant turns (and any trapped
13
+ * error events) — typically by subscribing to `SessionMonitor.onSessionEvent`
14
+ * and filtering on `providerID ∈ {zai, zai-coding-plan}`.
14
15
  *
15
16
  * Optionally writes snapshots + history samples for cross-session cache
16
17
  * and trend visualisation, mirroring `CodexQuotaWatcher`.
@@ -250,7 +251,7 @@ class ZaiQuotaWatcher {
250
251
  const cutoff = this.now() - SEVEN_DAY_MS;
251
252
  if (this.turns.length === 0)
252
253
  return;
253
- const next = this.turns.filter(turn => turn.timestampMs >= cutoff);
254
+ const next = this.turns.filter((turn) => turn.timestampMs >= cutoff);
254
255
  if (next.length !== this.turns.length) {
255
256
  this.turns = next;
256
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.21.1",
3
+ "version": "0.21.3",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -55,6 +55,8 @@
55
55
  "clean": "rm -rf dist",
56
56
  "lint": "eslint src/",
57
57
  "lint:fix": "eslint src/ --fix",
58
+ "format": "prettier --config ../prettier.config.cjs --ignore-path ../.prettierignore --write \"src/**/*.{ts,tsx,js,jsx,json,md,css,html,yml,yaml}\" \"*.{js,cjs,mjs,json,md,yml,yaml}\"",
59
+ "format:check": "prettier --config ../prettier.config.cjs --ignore-path ../.prettierignore --check \"src/**/*.{ts,tsx,js,jsx,json,md,css,html,yml,yaml}\" \"*.{js,cjs,mjs,json,md,yml,yaml}\"",
58
60
  "pretest": "npm run build",
59
61
  "test": "vitest run",
60
62
  "test:watch": "vitest",
@@ -64,9 +66,10 @@
64
66
  "@eslint/js": "^9.0.0",
65
67
  "@types/node": "^22.0.0",
66
68
  "eslint": "^9.0.0",
69
+ "prettier": "3.8.4",
67
70
  "typescript": "^5.7.0",
68
71
  "typescript-eslint": "^8.0.0",
69
- "vitest": "^3.0.0"
72
+ "vitest": "^4.1.9"
70
73
  },
71
74
  "license": "MIT",
72
75
  "dependencies": {