sidekick-shared 0.26.2 → 0.26.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to sidekick-shared will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.26.4] - 2026-09-09
9
+
10
+ ### Fixed
11
+
12
+ - `diagnoseProviderFailure()` recognizes explicit login expiry and reauthentication requests, and gives refresh-token rejection precedence over conversation-expiry wording while preserving structured evidence and credential-aware recovery.
13
+ - `Permission denied by policy` is classified as execution-policy denial, including HTTP 403 responses. Ambiguous session and permission messages retain their existing classifications.
14
+
15
+ ## [0.26.3] - 2026-09-09
16
+
17
+ ### Added
18
+
19
+ - `diagnoseProviderFailure()` and its pure types are available from the root and browser entry points. Results contain bounded diagnosis/recovery identifiers, evidence provenance, and optional HTTP/retry-after data without raw errors or credentials.
20
+ - `fetchProviderServiceStatus()` is available from the root and Node entry points, with pure result types also exported through browser. Stateless official-feed checks support cancellation and a ten-second deadline, preserve component/incident associations and independent timestamps, and distinguish observed, unavailable, and omitted incident evidence.
21
+ - `HealthReport.serviceStatus` and `DoctorOptions.fetchServiceStatuses` add explicit service evidence while retaining legacy status output and injection. `toLegacyProviderStatus()` supports compatibility projections.
22
+
23
+ ### Changed
24
+
25
+ - Doctor reports unavailable and partial public evidence explicitly. Existing `ErrorCategory`, quota guidance, and legacy status functions/types remain compatible; consumers needing availability semantics should adopt the new API.
26
+
8
27
  ## [0.26.2] - 2026-09-06
9
28
 
10
29
  ### Added
package/README.md CHANGED
@@ -9,6 +9,7 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **0.26.3–0.26.4: provider failure diagnosis and service status** — `diagnoseProviderFailure()` (root and browser) classifies caller-designated terminal errors into bounded diagnosis and recovery identifiers with evidence provenance; `fetchProviderServiceStatus()` (root and Node) returns explicit observed/unavailable public status with component and incident associations. 0.26.4 recognizes expired-login and re-authentication messages, gives explicit refresh-token rejection precedence over conversation-expiry wording, and classifies `denied by policy` as `execution_policy_denied`.
12
13
  - **0.26.2: reliable monitoring and search** — complete-line JSONL cursors preserve Unicode and Codex parser context; watcher `onBatchComplete` signals a delivered batch. `searchSessions()` adds workspace-path filtering and cancellation, and Doctor accepts an explicit session provider.
13
14
  - **One token vocabulary and cost provenance (0.26.0)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` shares quota resolution across CLI, MCP, and dashboard startup, with default local-sample preferences and an API-first option used by one-shot Codex queries; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
14
15
  - **Host-safe APIs (0.25.0)** — `listSessionPreviewsAsync()` / `readSessionPreviewAsync()` read previews with bounded concurrency and cooperative yielding; `ObservedSessionCollector.subscribe()` and `SessionMonitor.subscribe()` replace host polling loops with debounced change batches; `createSessionProviders({ onDiagnostic })` constructs every usable provider with structured diagnostics instead of throwing; `findSessionById()` resolves one session without scanning; `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` and `registerModelAlias()` transfer context/pricing resolutions across realms; `onAccountsChanged()` reports login/logout/switch events; provider constructors perform no environment I/O, and quota pollers stay dormant without an account.
@@ -449,20 +450,83 @@ const tasks = await readTasks(project, { status: 'pending' });
449
450
  console.log(`Found ${tasks.length} tasks`);
450
451
  ```
451
452
 
452
- ### Check provider status
453
+ ### Diagnose terminal provider failures
454
+
455
+ `diagnoseProviderFailure()` is a pure, synchronous helper exported from `sidekick-shared` and `sidekick-shared/browser`. The caller decides whether an SDK event is terminal: a Codex `ErrorItem`, including a reconnect notice, must not become terminal merely because its message can be classified.
453
456
 
454
457
  ```typescript
455
- import { fetchProviderStatus } from 'sidekick-shared';
458
+ import { diagnoseProviderFailure } from 'sidekick-shared/browser';
456
459
 
457
- const status = await fetchProviderStatus();
458
- if (status.indicator !== 'none') {
459
- console.log(`Claude API: ${status.description}`);
460
- for (const c of status.affectedComponents) {
461
- console.log(` ${c.name}: ${c.status}`);
462
- }
460
+ function diagnoseTerminalError(error: unknown) {
461
+ return diagnoseProviderFailure({
462
+ provider: 'codex', // AccountProviderId: 'claude-code' | 'codex'
463
+ credentialKind: 'oauth', // 'oauth' | 'api-key' | 'unknown'
464
+ error,
465
+ authentication: {
466
+ credentialKind: 'oauth',
467
+ state: 'authenticated',
468
+ source: 'local-check',
469
+ checkedAt: '2026-09-09T12:00:00.000Z',
470
+ },
471
+ });
472
+ }
473
+ ```
474
+
475
+ Authentication evidence is optional. Its states are `authenticated`, `missing`, `signed-out`, `expired`, `rejected`, and `unknown`; sources are `local-check` and `provider-response`. The caller owns the observation's freshness and relevance. File presence alone does not establish authenticated request success.
476
+
477
+ The result contains `provider`, caller-supplied `credentialKind`, `diagnosis`, `recovery`, and `evidence`. Evidence records a bounded rule and its source (`structured-error`, `http-status`, `message`, or `authentication-check`); authentication-check evidence includes `checkedAt`. Valid supplied authentication evidence is preserved separately. Optional `httpStatus` and `retryAfter` retain request information; retry-after is `{ kind: 'delay', delayMs }` or `{ kind: 'date', at }`, without a clock-dependent conversion.
478
+
479
+ | Diagnosis | Recovery identifier |
480
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
481
+ | `missing_credentials` | `sign_in`, `update_credentials`, or `check_authentication`, according to credential kind |
482
+ | `oauth_reauthentication_required` | `sign_in` |
483
+ | `api_credentials_rejected` | `update_credentials` |
484
+ | `authentication_rejected` (credential kind unknown) | `check_authentication` |
485
+ | `invalid_provider_session` | `start_new_session` |
486
+ | `service_unavailable`, `timeout`, `rate_limited`, `overloaded` | `retry_later` |
487
+ | `connection_failed` | `check_connection` |
488
+ | `execution_policy_denied` | `review_execution_policy` |
489
+ | `runtime_unavailable` | `repair_runtime` |
490
+ | `context_overflow` | `reduce_context` |
491
+ | `unknown` | `inspect_error` |
492
+
493
+ Recognized structured request codes take precedence over HTTP status, followed by narrow message patterns. A bare 401 establishes credential rejection, with recovery chosen from the credential kind; a bare 403 does not distinguish authentication from execution policy and remains unknown, while a message containing `denied by policy` is `execution_policy_denied` regardless of status. An explicit OAuth or refresh-token rejection (`refresh token is invalid`) outranks overlapping thread/conversation-expiry wording; `login has expired` and `please re-authenticate` resolve according to the credential kind. Ambiguous phrases such as `Session expired` or `Permission denied` on their own remain unknown. Explicit credential rejection from the request overrides a successful local OAuth check. OAuth observations never validate API keys. Thread/conversation failures, 5xx responses, DNS failures, and connection resets do not establish rejected account credentials. The reported `Reconnecting... 2/5 (unexpected status 503 Service Unavailable: upstream connect error or disconnect/reset before headers. reset reason: connection termination)` is classified as `service_unavailable`, with message provenance and HTTP 503, if the caller supplies it as a terminal error.
494
+
495
+ The projection contains no raw errors, stacks, credentials, or request URLs. Product wording belongs to consumers. The helper performs no I/O, credential reads, SDK calls, stream control, retries, or UI actions. Existing transcript/tool `ErrorCategory` and quota-specific guidance contracts remain unchanged.
496
+
497
+ ### Check public provider service status
498
+
499
+ ```typescript
500
+ import { fetchProviderServiceStatus } from 'sidekick-shared/node';
501
+ import type { ProviderServiceStatus } from 'sidekick-shared/browser';
502
+
503
+ const controller = new AbortController();
504
+ const status: ProviderServiceStatus = await fetchProviderServiceStatus('codex', {
505
+ signal: controller.signal,
506
+ });
507
+
508
+ if (status.availability === 'unavailable') {
509
+ console.log('Public status unavailable', status.reason, status.checkedAt);
510
+ } else {
511
+ console.log(status.severity, status.checkedAt, status.providerUpdatedAt);
512
+ // null means the feed omitted incident evidence; [] explicitly reports none unresolved.
513
+ console.log(status.incidents === null ? 'Incident information unavailable' : status.incidents);
463
514
  }
464
515
  ```
465
516
 
517
+ The fetcher is exported from root and Node, with pure result types also exported from browser. It requests the official [Claude summary](https://status.claude.com/api/v2/summary.json) or [OpenAI summary](https://status.openai.com/api/v2/summary.json) without credentials. Every call has a ten-second deadline covering headers and body consumption, supports caller cancellation, and performs one request with no retries or cache.
518
+
519
+ - `observed` results contain provider, source URL, check timestamp, reported severity/description, provider page update timestamp, all reported components, and unresolved incidents. Supported severity values are `none`, `minor`, `major`, `critical`, and `maintenance`.
520
+ - Components retain IDs, names, and reported states. Incidents retain IDs, titles, status, impact, HTTP(S) links, provider update timestamps, and component IDs. Unknown associations are `null`; unmatched IDs are preserved. Resolved and postmortem incidents are excluded.
521
+ - Omitted incident data is represented as `incidents: null`, preserving a partial observation without claiming zero incidents. Missing provider timestamps and links remain `null`; the check timestamp never substitutes for a provider update.
522
+ - `unavailable` results contain no severity. Reasons are `http_error`, `network_error`, `timeout`, `cancelled`, or `invalid_response`, with optional HTTP status and no raw error text. Malformed supplied fields are rejected rather than defaulted to operational.
523
+
524
+ Request failure, authentication, CLI readiness, and public service status are separate observations. Public incidents supplement a diagnosis but do not prove a failed request's cause. An operational public page does not prove that the user's connection works. Preserve component associations to avoid attributing unrelated vendor incidents to Claude Code or Codex; unknown component mappings and custom provider endpoints remain uncorrelated. The result's `provider` identifies the selected public feed, not a verified request-backend mapping.
525
+
526
+ Consumers own caching, stale-evidence presentation, polling, and request coalescing. Neither API initiates sign-in, modifies credentials, switches providers, requests a model response, or replays a turn.
527
+
528
+ `fetchProviderStatus()`, `fetchOpenAIStatus()`, and `ProviderStatusState` remain compatible. Their historical fallback combines `indicator: 'none'` with `Status unavailable`; migrate to the new API for reliable availability semantics. `toLegacyProviderStatus()` is a lossy compatibility projection for older output contracts, not an availability check. Doctor includes optional `serviceStatus` evidence alongside legacy `providerStatus`; its new `fetchServiceStatuses` injection takes precedence over legacy `fetchStatuses`, which remains supported without additional network requests.
529
+
466
530
  ### Check Claude peak-hours state
467
531
 
468
532
  ```typescript
package/dist/browser.d.ts CHANGED
@@ -39,3 +39,6 @@ export { applySessionSummary, isFileImported, markFileImported, removeSessionSum
39
39
  export type { ApplySessionSummaryOptions, SessionSummaryFromStatsOptions } from './historicalStore';
40
40
  export { FAILING_TOOL_TREND_ARROWS, failingToolTrend, mergeFailingToolWindows, } from './failingTools';
41
41
  export type { FailingToolTrend, FailingToolTrendRow, FailingToolWindow } from './failingTools';
42
+ export { diagnoseProviderFailure } from './providerFailure';
43
+ export type { ProviderCredentialKind, ProviderAuthenticationEvidence, ProviderFailureInput, ProviderFailureCode, ProviderRecoveryAction, ProviderRetryAfter, ProviderFailureEvidence, ProviderFailureDiagnosis, } from './providerFailure';
44
+ export type { ProviderServiceSeverity, ProviderServiceComponent, ProviderServiceIncident, ProviderServiceStatusFailureReason, ObservedProviderServiceStatus, UnavailableProviderServiceStatus, ProviderServiceStatus, FetchProviderServiceStatusOptions, } from './providerServiceStatusTypes';
package/dist/browser.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = exports.resolveModelAlias = exports.registerModelAlias = exports.importResolvedModelCatalog = exports.exportResolvedModelCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.resolveModelPricing = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.SESSION_PROVIDER_IDS = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.QUOTA_PROVIDER_IDS = exports.MODEL_PROVIDER_IDS = exports.ACCOUNT_PROVIDER_IDS = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.parseMcpToolName = exports.parseLocalDateKey = exports.formatTokenCount = exports.formatLocalDateKey = exports.formatDurationMs = exports.addLocalDays = exports.projectSessionTranscript = exports.estimateSerializedTokens = exports.estimateTextTokens = exports.TOKEN_TOTAL_LABEL = exports.TOKEN_CONTEXT_LABEL = exports.sumTokenTotals = exports.summarizeTokens = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.DEFAULT_CONTEXT_WINDOW = exports.resolveModelContextWindow = exports.getModelContextWindowSize = void 0;
13
- exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = void 0;
13
+ exports.diagnoseProviderFailure = exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = void 0;
14
14
  var modelContext_1 = require("./modelContext");
15
15
  Object.defineProperty(exports, "getModelContextWindowSize", { enumerable: true, get: function () { return modelContext_1.getModelContextWindowSize; } });
16
16
  Object.defineProperty(exports, "resolveModelContextWindow", { enumerable: true, get: function () { return modelContext_1.resolveModelContextWindow; } });
@@ -92,3 +92,5 @@ var failingTools_1 = require("./failingTools");
92
92
  Object.defineProperty(exports, "FAILING_TOOL_TREND_ARROWS", { enumerable: true, get: function () { return failingTools_1.FAILING_TOOL_TREND_ARROWS; } });
93
93
  Object.defineProperty(exports, "failingToolTrend", { enumerable: true, get: function () { return failingTools_1.failingToolTrend; } });
94
94
  Object.defineProperty(exports, "mergeFailingToolWindows", { enumerable: true, get: function () { return failingTools_1.mergeFailingToolWindows; } });
95
+ var providerFailure_1 = require("./providerFailure");
96
+ Object.defineProperty(exports, "diagnoseProviderFailure", { enumerable: true, get: function () { return providerFailure_1.diagnoseProviderFailure; } });
package/dist/doctor.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ActiveAccountStatus } from './accountStatus';
2
2
  import { type ProjectIdentity } from './paths';
3
3
  import { type ProviderStatusState } from './providerStatus';
4
+ import type { ProviderServiceStatus } from './providerServiceStatusTypes';
4
5
  import type { ProviderId } from './providers/types';
5
6
  import { type OpenCodeDbRuntimeStatus } from './providers/openCodeDatabase';
6
7
  export type HealthCheckStatus = 'ok' | 'warning' | 'error' | 'info';
@@ -39,6 +40,10 @@ export interface HealthReport {
39
40
  claude: ProviderStatusState;
40
41
  openai: ProviderStatusState;
41
42
  };
43
+ serviceStatus?: {
44
+ claude: ProviderServiceStatus;
45
+ openai: ProviderServiceStatus;
46
+ };
42
47
  /** Legacy Claude Code path diagnostics, retained for compatibility. */
43
48
  sessions: SessionDiagnostics;
44
49
  sessionProvider?: ProviderId;
@@ -49,6 +54,11 @@ export interface DoctorOptions {
49
54
  provider?: ProviderId | 'auto';
50
55
  deprecatedSettings?: DeprecatedSetting[];
51
56
  openCodeStatus?: OpenCodeDbRuntimeStatus;
57
+ fetchServiceStatuses?: () => Promise<{
58
+ claude: ProviderServiceStatus;
59
+ openai: ProviderServiceStatus;
60
+ }>;
61
+ /** Legacy injection; does not establish service evidence availability. */
52
62
  fetchStatuses?: () => Promise<{
53
63
  claude: ProviderStatusState;
54
64
  openai: ProviderStatusState;
package/dist/doctor.js CHANGED
@@ -43,6 +43,7 @@ const accountStatus_1 = require("./accountStatus");
43
43
  const paths_1 = require("./paths");
44
44
  const sessionPathResolver_1 = require("./parsers/sessionPathResolver");
45
45
  const providerStatus_1 = require("./providerStatus");
46
+ const providerServiceStatus_1 = require("./providerServiceStatus");
46
47
  const openCode_1 = require("./providers/openCode");
47
48
  const factory_1 = require("./providers/factory");
48
49
  const detect_1 = require("./providers/detect");
@@ -163,11 +164,34 @@ async function runDoctor(options = {}) {
163
164
  openCode = database.getRuntimeStatus();
164
165
  database.close();
165
166
  }
166
- const providerStatus = await (options.fetchStatuses ??
167
- (async () => {
168
- const [claude, openai] = await Promise.all([(0, providerStatus_1.fetchProviderStatus)(), (0, providerStatus_1.fetchOpenAIStatus)()]);
169
- return { claude, openai };
170
- }))().catch(() => ({ claude: statusUnavailable(), openai: statusUnavailable() }));
167
+ const serviceStatus = options.fetchServiceStatuses || !options.fetchStatuses
168
+ ? await (options.fetchServiceStatuses ??
169
+ (async () => {
170
+ const [claude, openai] = await Promise.all([
171
+ (0, providerServiceStatus_1.fetchProviderServiceStatus)('claude-code'),
172
+ (0, providerServiceStatus_1.fetchProviderServiceStatus)('codex'),
173
+ ]);
174
+ return { claude, openai };
175
+ }))().catch(() => {
176
+ const unavailable = (provider) => ({
177
+ availability: 'unavailable',
178
+ provider,
179
+ checkedAt: new Date().toISOString(),
180
+ sourceUrl: `https://status.${provider === 'codex' ? 'openai' : 'claude'}.com/api/v2/summary.json`,
181
+ reason: 'network_error',
182
+ });
183
+ return { claude: unavailable('claude-code'), openai: unavailable('codex') };
184
+ })
185
+ : undefined;
186
+ const providerStatus = serviceStatus
187
+ ? {
188
+ claude: (0, providerStatus_1.toLegacyProviderStatus)(serviceStatus.claude),
189
+ openai: (0, providerStatus_1.toLegacyProviderStatus)(serviceStatus.openai),
190
+ }
191
+ : await options.fetchStatuses().catch(() => ({
192
+ claude: statusUnavailable(),
193
+ openai: statusUnavailable(),
194
+ }));
171
195
  const checks = [];
172
196
  checks.push(project.hasLegacyMismatch
173
197
  ? {
@@ -225,25 +249,48 @@ async function runDoctor(options = {}) {
225
249
  }
226
250
  : {}),
227
251
  });
228
- const unavailableProviders = Object.entries(providerStatus)
229
- .filter(([, status]) => status.description === 'Status unavailable')
230
- .map(([name]) => name);
231
- const degradedProviders = Object.entries(providerStatus)
232
- .filter(([, status]) => status.indicator !== 'none')
233
- .map(([name]) => name);
252
+ const unavailableProviders = serviceStatus
253
+ ? Object.entries(serviceStatus)
254
+ .filter(([, status]) => status.availability === 'unavailable')
255
+ .map(([name]) => name)
256
+ : Object.entries(providerStatus)
257
+ .filter(([, status]) => status.description === 'Status unavailable')
258
+ .map(([name]) => name);
259
+ const degradedProviders = serviceStatus
260
+ ? Object.entries(serviceStatus)
261
+ .filter(([, status]) => status.availability === 'observed' && status.severity !== 'none')
262
+ .map(([name]) => name)
263
+ : Object.entries(providerStatus)
264
+ .filter(([, status]) => status.indicator !== 'none')
265
+ .map(([name]) => name);
266
+ const partialProviders = serviceStatus
267
+ ? Object.entries(serviceStatus)
268
+ .filter(([, status]) => status.availability === 'observed' && status.incidents === null)
269
+ .map(([name]) => name)
270
+ : [];
271
+ const observations = [
272
+ ...(degradedProviders.length
273
+ ? [`Public vendor degradation reported for: ${degradedProviders.join(', ')}.`]
274
+ : []),
275
+ ...(unavailableProviders.length
276
+ ? [`Status pages unavailable for: ${unavailableProviders.join(', ')}.`]
277
+ : []),
278
+ ...(partialProviders.length
279
+ ? [`Incident information unavailable for: ${partialProviders.join(', ')}.`]
280
+ : []),
281
+ ];
234
282
  checks.push({
235
283
  id: 'provider_api',
236
284
  status: degradedProviders.includes(sessionProvider === 'codex' ? 'openai' : sessionProvider === 'claude-code' ? 'claude' : '')
237
285
  ? 'warning'
238
- : degradedProviders.length > 0 || unavailableProviders.length > 0
286
+ : observations.length
239
287
  ? 'info'
240
288
  : 'ok',
241
- title: 'Provider API status',
242
- message: degradedProviders.length > 0
243
- ? `Provider incidents reported for: ${degradedProviders.join(', ')}.`
244
- : unavailableProviders.length > 0
245
- ? `Status pages unavailable for: ${unavailableProviders.join(', ')}.`
246
- : 'Claude and OpenAI status pages report normal operation.',
289
+ title: 'Public provider service status',
290
+ message: (observations.length
291
+ ? observations.join(' ')
292
+ : 'Claude and OpenAI status pages report normal operation.') +
293
+ ' Public status does not establish request connectivity or authentication.',
247
294
  });
248
295
  const deprecatedSettings = options.deprecatedSettings ?? [];
249
296
  checks.push({
@@ -275,6 +322,7 @@ async function runDoctor(options = {}) {
275
322
  accounts,
276
323
  openCode,
277
324
  providerStatus,
325
+ ...(serviceStatus ? { serviceStatus } : {}),
278
326
  sessions,
279
327
  sessionProvider,
280
328
  };
package/dist/index.d.ts CHANGED
@@ -233,7 +233,7 @@ export { quotaHistoryRuntimeProviderSchema, quotaHistorySampleSchema, quotaHisto
233
233
  export { activeProviderAccountStatusSchema, activeAccountStatusSchema, } from './schemas/accountStatus';
234
234
  export { accountProviderIdSchema, accountManagerResultSchema, beginAccountLoginResultSchema, accountLoginStatusSchema, accountEntrySchema, savedAccountProfileSchema, listAllAccountsResultSchema, } from './schemas/accountManager';
235
235
  export { assistantTurnEventSchema, assistantTurnEventTypeSchema, assistantTurnNarrationStepSchema, assistantTurnProcessSchema, assistantTurnProcessStepSchema, assistantTurnProjectionSchema, assistantTurnReasoningTimelineItemSchema, assistantTurnSubagentSchema, assistantTurnSubagentStatusSchema, assistantTurnTimelineItemSchema, assistantTurnToolGroupStepSchema, assistantTurnToolRefSchema, } from './schemas/assistantTurn';
236
- export { fetchProviderStatus, fetchOpenAIStatus } from './providerStatus';
236
+ export { fetchProviderStatus, fetchOpenAIStatus, toLegacyProviderStatus } from './providerStatus';
237
237
  export type { ProviderStatusState } from './providerStatus';
238
238
  export { formatHealthReport, getSessionDiagnostics, runDoctor } from './doctor';
239
239
  export type { DeprecatedSetting, DoctorOptions, HealthCheck, HealthCheckStatus, HealthReport, SessionDiagnostics, } from './doctor';
@@ -241,3 +241,7 @@ export { _resetPeakHoursCache, createPeakHoursNotApplicableState, DEFAULT_PEAK_H
241
241
  export type { FetchPeakHoursOptions, PeakHoursState } from './peakHours';
242
242
  export { FAILING_TOOL_TREND_ARROWS, failingToolTrend, mergeFailingToolWindows, } from './failingTools';
243
243
  export type { FailingToolTrend, FailingToolTrendRow, FailingToolWindow } from './failingTools';
244
+ export { diagnoseProviderFailure } from './providerFailure';
245
+ export type { ProviderCredentialKind, ProviderAuthenticationEvidence, ProviderFailureInput, ProviderFailureCode, ProviderRecoveryAction, ProviderRetryAfter, ProviderFailureEvidence, ProviderFailureDiagnosis, } from './providerFailure';
246
+ export type { ProviderServiceSeverity, ProviderServiceComponent, ProviderServiceIncident, ProviderServiceStatusFailureReason, ObservedProviderServiceStatus, UnavailableProviderServiceStatus, ProviderServiceStatus, FetchProviderServiceStatusOptions, } from './providerServiceStatusTypes';
247
+ export { fetchProviderServiceStatus } from './providerServiceStatus';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ exports.finalizeAccountLogin = exports.getAccountLoginStatusAsync = exports.getA
11
11
  exports.FIVE_HOUR_WINDOW_MS = exports.AutoSwitchController = exports.decideAutoSwitch = exports.DEFAULT_AUTO_SWITCH_CONFIG = exports.removeLauncher = exports.writeLauncher = exports.isShellHookInstalled = exports.uninstallShellHook = exports.installShellHook = exports.setTerminalActiveProfile = exports.removeCodexAccount = exports.switchToCodexAccountAsync = exports.switchToCodexAccount = exports.finalizeCodexAccountAsync = exports.finalizeCodexAccount = exports.prepareCodexAccountAsync = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.resolveActiveCodexAccount = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.readClaudeProfileIdentity = exports.isClaudeProfileAuthenticated = exports.claudeKeychainService = exports.claudeKeychainSuffix = exports.getClaudeProfileHome = exports.getClaudeProfilesDir = exports.onAccountsChanged = exports.getActiveAccountStatus = exports.ACCOUNT_PROVIDER_IDS = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.resolveClaudeLoginCommand = exports.listAllAccounts = exports.switchAccountAsync = exports.switchAccount = exports.spawnAccountLogin = exports.finalizeAccountLoginAsync = void 0;
12
12
  exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.resolveModelPricing = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.resolveModelContextWindow = exports.getModelContextWindowSize = exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.MultiProviderQuotaService = exports.resolveZaiQuota = exports.readZaiCredentials = exports.quotaStateFromZaiQuotaLimitPayload = exports.fetchZaiQuotaFromApi = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = exports.fetchCodexQuotaFromApi = exports.fetchCodexResetCreditsFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = exports.appendQuotaHistorySample = exports.quotaFromStatuslinePayload = exports.parseClaudeStatuslinePayload = exports.evaluateQuotaThresholds = exports.describeQuotaThresholdAlert = exports.DEFAULT_QUOTA_THRESHOLDS = exports.QUOTA_FRESH_MAX_AGE_MS = exports.QUOTA_AGING_MAX_AGE_MS = exports.formatQuotaAge = exports.classifyQuotaFreshness = exports.resolveQuota = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.withQuotaProjections = exports.projectQuotaWindow = exports.fetchQuota = exports.SEVEN_DAY_WINDOW_MS = exports.QUOTA_PROVIDER_IDS = void 0;
13
13
  exports.providerQuotaStateSchema = exports.runtimeQuotaProviderSchema = exports.quotaFailureDescriptorSchema = exports.peakHoursStateSchema = exports.codexResetCreditsSnapshotSchema = exports.codexResetCreditSchema = exports.quotaSourceSchema = exports.quotaProviderIdSchema = exports.quotaFailureKindSchema = exports.quotaStateSchema = exports.quotaWindowSchema = exports.extractSessionEvents = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.ToolCallTracker = exports.extractToolCalls = exports.extractToolCall = exports.extractErrorMessage = exports.categorizeError = exports.gatherAssetsForCwd = exports.codexSessions = exports.readCodexAssets = exports.claudeSessions = exports.readClaudeAssets = exports.extractCommands = exports.extractFilePaths = exports.extractUrls = exports.extractTokenUsage = exports.getObservedContextWindowPath = exports.recordObservedContextWindow = exports.loadObservedContextWindows = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmContextWindows = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = exports.resolveModelAlias = exports.registerModelAlias = exports.importResolvedModelCatalog = exports.exportResolvedModelCatalog = exports.MODEL_PROVIDER_IDS = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = void 0;
14
- exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.scopePeakHoursToSessionProvider = exports.PEAK_HOURS_DESCRIPTION = exports.PEAK_HOURS_CACHE_MS = exports.isClaudeCodeSessionProvider = exports.getScheduledPeakHoursState = exports.fetchPeakHoursStatus = exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = exports.createPeakHoursNotApplicableState = exports._resetPeakHoursCache = exports.runDoctor = exports.getSessionDiagnostics = exports.formatHealthReport = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.assistantTurnToolRefSchema = exports.assistantTurnToolGroupStepSchema = exports.assistantTurnTimelineItemSchema = exports.assistantTurnSubagentStatusSchema = exports.assistantTurnSubagentSchema = exports.assistantTurnReasoningTimelineItemSchema = exports.assistantTurnProjectionSchema = exports.assistantTurnProcessStepSchema = exports.assistantTurnProcessSchema = exports.assistantTurnNarrationStepSchema = exports.assistantTurnEventTypeSchema = exports.assistantTurnEventSchema = exports.listAllAccountsResultSchema = exports.savedAccountProfileSchema = exports.accountEntrySchema = exports.accountLoginStatusSchema = exports.beginAccountLoginResultSchema = exports.accountManagerResultSchema = exports.accountProviderIdSchema = exports.activeAccountStatusSchema = exports.activeProviderAccountStatusSchema = exports.quotaHistoryDailyBucketSchema = exports.quotaHistorySampleSchema = exports.quotaHistoryRuntimeProviderSchema = exports.providerQuotaMapSchema = exports.zaiProviderQuotaStateSchema = exports.codexProviderQuotaStateSchema = exports.claudeProviderQuotaStateSchema = void 0;
14
+ exports.fetchProviderServiceStatus = exports.diagnoseProviderFailure = exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.scopePeakHoursToSessionProvider = exports.PEAK_HOURS_DESCRIPTION = exports.PEAK_HOURS_CACHE_MS = exports.isClaudeCodeSessionProvider = exports.getScheduledPeakHoursState = exports.fetchPeakHoursStatus = exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = exports.createPeakHoursNotApplicableState = exports._resetPeakHoursCache = exports.runDoctor = exports.getSessionDiagnostics = exports.formatHealthReport = exports.toLegacyProviderStatus = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.assistantTurnToolRefSchema = exports.assistantTurnToolGroupStepSchema = exports.assistantTurnTimelineItemSchema = exports.assistantTurnSubagentStatusSchema = exports.assistantTurnSubagentSchema = exports.assistantTurnReasoningTimelineItemSchema = exports.assistantTurnProjectionSchema = exports.assistantTurnProcessStepSchema = exports.assistantTurnProcessSchema = exports.assistantTurnNarrationStepSchema = exports.assistantTurnEventTypeSchema = exports.assistantTurnEventSchema = exports.listAllAccountsResultSchema = exports.savedAccountProfileSchema = exports.accountEntrySchema = exports.accountLoginStatusSchema = exports.beginAccountLoginResultSchema = exports.accountManagerResultSchema = exports.accountProviderIdSchema = exports.activeAccountStatusSchema = exports.activeProviderAccountStatusSchema = exports.quotaHistoryDailyBucketSchema = exports.quotaHistorySampleSchema = exports.quotaHistoryRuntimeProviderSchema = exports.providerQuotaMapSchema = exports.zaiProviderQuotaStateSchema = exports.codexProviderQuotaStateSchema = exports.claudeProviderQuotaStateSchema = void 0;
15
15
  var usageNormalization_1 = require("./usageNormalization");
16
16
  Object.defineProperty(exports, "normalizeProviderUsage", { enumerable: true, get: function () { return usageNormalization_1.normalizeProviderUsage; } });
17
17
  Object.defineProperty(exports, "extractNormalizedUsage", { enumerable: true, get: function () { return usageNormalization_1.extractNormalizedUsage; } });
@@ -615,6 +615,7 @@ Object.defineProperty(exports, "assistantTurnToolRefSchema", { enumerable: true,
615
615
  var providerStatus_1 = require("./providerStatus");
616
616
  Object.defineProperty(exports, "fetchProviderStatus", { enumerable: true, get: function () { return providerStatus_1.fetchProviderStatus; } });
617
617
  Object.defineProperty(exports, "fetchOpenAIStatus", { enumerable: true, get: function () { return providerStatus_1.fetchOpenAIStatus; } });
618
+ Object.defineProperty(exports, "toLegacyProviderStatus", { enumerable: true, get: function () { return providerStatus_1.toLegacyProviderStatus; } });
618
619
  // Shared diagnostics / health report
619
620
  var doctor_1 = require("./doctor");
620
621
  Object.defineProperty(exports, "formatHealthReport", { enumerable: true, get: function () { return doctor_1.formatHealthReport; } });
@@ -635,3 +636,7 @@ var failingTools_1 = require("./failingTools");
635
636
  Object.defineProperty(exports, "FAILING_TOOL_TREND_ARROWS", { enumerable: true, get: function () { return failingTools_1.FAILING_TOOL_TREND_ARROWS; } });
636
637
  Object.defineProperty(exports, "failingToolTrend", { enumerable: true, get: function () { return failingTools_1.failingToolTrend; } });
637
638
  Object.defineProperty(exports, "mergeFailingToolWindows", { enumerable: true, get: function () { return failingTools_1.mergeFailingToolWindows; } });
639
+ var providerFailure_1 = require("./providerFailure");
640
+ Object.defineProperty(exports, "diagnoseProviderFailure", { enumerable: true, get: function () { return providerFailure_1.diagnoseProviderFailure; } });
641
+ var providerServiceStatus_1 = require("./providerServiceStatus");
642
+ Object.defineProperty(exports, "fetchProviderServiceStatus", { enumerable: true, get: function () { return providerServiceStatus_1.fetchProviderServiceStatus; } });
package/dist/node.d.ts CHANGED
@@ -38,3 +38,5 @@ export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, read
38
38
  export type { AsyncSessionPreviewOptions, ListSessionPreviewsAsyncOptions, ListSessionPreviewsOptions, ReadSessionPreviewAsyncOptions, ReadSessionPreviewOptions, SessionPreview, SessionPreviewListResult, SessionPreviewReadResult, } from './sessionPreviews';
39
39
  export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, fileFingerprintParts, } from './observedSessionCollector';
40
40
  export type { KnownObservedSessionFingerprint, ObservedSessionChange, ObservedSessionChangeBatch, ObservedSessionChangeType, ObservedSessionReference, ObservedSessionFingerprintParts, ObservedSessionSourceSubscribeOptions, ObservedSessionSubscribeOptions, ObservedSessionCollectionSource, ProviderObservedSessionCollectionSource, ObservedSessionDiagnosticKind, ObservedSessionDiagnosticPhase, ObservedSessionDiagnosticSeverity, ObservedSessionDiagnostic, ObservedSessionCollection, ObservedSessionCollectorOptions, } from './observedSessionCollector';
41
+ export type { ProviderServiceSeverity, ProviderServiceComponent, ProviderServiceIncident, ProviderServiceStatusFailureReason, ObservedProviderServiceStatus, UnavailableProviderServiceStatus, ProviderServiceStatus, FetchProviderServiceStatusOptions, } from './providerServiceStatusTypes';
42
+ export { fetchProviderServiceStatus } from './providerServiceStatus';
package/dist/node.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.readSessionPreviewAsync = exports.readSessionPreview = exports.listSessionPreviewsAsync = exports.listSessionPreviews = exports.readCodexHistory = exports.walkRolloutFilesAsync = exports.walkRolloutFiles = exports.findCodexRolloutFile = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.importSessionHistory = exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.pruneUsageCache = exports.getUsageCacheDir = exports.collectUsageEvents = exports.USAGE_CACHE_VERSION = exports.MAX_USAGE_CACHE_FILES = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = exports.writeStateFile = exports.readStateFile = exports.quotaToStateFile = exports.getStateFilePath = exports.billingBlockToStateFile = exports.STATE_FILE_SCHEMA_VERSION = exports.STATE_FILE_NAME = exports.sessionFingerprintParts = exports.fingerprintString = exports.readSessionReportInputs = exports.readSessionFileStats = exports.providerContextSizeFn = exports.firstUserPrompt = exports.computeSessionFileStats = exports.readSessionTranscript = exports.listRecentSessions = exports.getObservedContextWindowPath = exports.recordObservedContextWindow = exports.loadObservedContextWindows = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmContextWindows = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = void 0;
11
- exports.fileFingerprintParts = exports.fileFingerprint = exports.observedSessionSourceFromProvider = exports.ObservedSessionCollector = void 0;
11
+ exports.fetchProviderServiceStatus = exports.fileFingerprintParts = exports.fileFingerprint = exports.observedSessionSourceFromProvider = exports.ObservedSessionCollector = void 0;
12
12
  var pricingCatalog_1 = require("./pricingCatalog");
13
13
  Object.defineProperty(exports, "hydratePricingCatalog", { enumerable: true, get: function () { return pricingCatalog_1.hydratePricingCatalog; } });
14
14
  Object.defineProperty(exports, "normalizeLiteLlmCatalog", { enumerable: true, get: function () { return pricingCatalog_1.normalizeLiteLlmCatalog; } });
@@ -80,3 +80,5 @@ Object.defineProperty(exports, "ObservedSessionCollector", { enumerable: true, g
80
80
  Object.defineProperty(exports, "observedSessionSourceFromProvider", { enumerable: true, get: function () { return observedSessionCollector_1.observedSessionSourceFromProvider; } });
81
81
  Object.defineProperty(exports, "fileFingerprint", { enumerable: true, get: function () { return observedSessionCollector_1.fileFingerprint; } });
82
82
  Object.defineProperty(exports, "fileFingerprintParts", { enumerable: true, get: function () { return observedSessionCollector_1.fileFingerprintParts; } });
83
+ var providerServiceStatus_1 = require("./providerServiceStatus");
84
+ Object.defineProperty(exports, "fetchProviderServiceStatus", { enumerable: true, get: function () { return providerServiceStatus_1.fetchProviderServiceStatus; } });
@@ -0,0 +1,46 @@
1
+ import type { AccountProviderId } from './providerIds';
2
+ export type ProviderCredentialKind = 'oauth' | 'api-key' | 'unknown';
3
+ export interface ProviderAuthenticationEvidence {
4
+ credentialKind: ProviderCredentialKind;
5
+ state: 'authenticated' | 'missing' | 'signed-out' | 'expired' | 'rejected' | 'unknown';
6
+ checkedAt: string;
7
+ source: 'local-check' | 'provider-response';
8
+ }
9
+ export interface ProviderFailureInput {
10
+ provider: AccountProviderId;
11
+ credentialKind: ProviderCredentialKind;
12
+ /** The caller, not this helper, determines whether the error is terminal. */
13
+ error: unknown;
14
+ /** The caller owns freshness and relevance of this separate observation. */
15
+ authentication?: ProviderAuthenticationEvidence;
16
+ }
17
+ export type ProviderFailureCode = 'missing_credentials' | 'oauth_reauthentication_required' | 'api_credentials_rejected' | 'authentication_rejected' | 'invalid_provider_session' | 'service_unavailable' | 'connection_failed' | 'timeout' | 'rate_limited' | 'overloaded' | 'execution_policy_denied' | 'runtime_unavailable' | 'context_overflow' | 'unknown';
18
+ export type ProviderRecoveryAction = 'sign_in' | 'update_credentials' | 'check_authentication' | 'start_new_session' | 'retry_later' | 'check_connection' | 'review_execution_policy' | 'repair_runtime' | 'reduce_context' | 'inspect_error';
19
+ export type ProviderRetryAfter = {
20
+ kind: 'delay';
21
+ delayMs: number;
22
+ } | {
23
+ kind: 'date';
24
+ at: string;
25
+ };
26
+ export interface ProviderFailureEvidence {
27
+ source: 'structured-error' | 'http-status' | 'message' | 'authentication-check';
28
+ rule: ProviderFailureCode;
29
+ checkedAt?: string;
30
+ }
31
+ /** Safe diagnostic projection. No raw error text, request data, or credentials. */
32
+ export interface ProviderFailureDiagnosis {
33
+ provider: AccountProviderId;
34
+ credentialKind: ProviderCredentialKind;
35
+ diagnosis: ProviderFailureCode;
36
+ recovery: ProviderRecoveryAction;
37
+ evidence: ProviderFailureEvidence[];
38
+ authentication?: ProviderAuthenticationEvidence;
39
+ httpStatus?: number;
40
+ retryAfter?: ProviderRetryAfter;
41
+ }
42
+ /**
43
+ * Classify a caller-designated terminal failure without I/O or recovery actions.
44
+ * Public status and runtime readiness do not establish request authentication.
45
+ */
46
+ export declare function diagnoseProviderFailure(input: ProviderFailureInput): ProviderFailureDiagnosis;
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diagnoseProviderFailure = diagnoseProviderFailure;
4
+ function read(value, key) {
5
+ try {
6
+ return value && typeof value === 'object' ? value[key] : undefined;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ function text(value) {
13
+ return typeof value === 'string' ? value.slice(0, 16_384) : '';
14
+ }
15
+ function statusNumber(value) {
16
+ const number = typeof value === 'string' && /^\d{3}$/.test(value) ? Number(value) : value;
17
+ return typeof number === 'number' && Number.isInteger(number) && number >= 100 && number <= 599
18
+ ? number
19
+ : undefined;
20
+ }
21
+ function timestamp(value) {
22
+ return typeof value === 'string' &&
23
+ value.length <= 64 &&
24
+ /^(?:\d{4}-\d{2}-\d{2}T|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), )/.test(value) &&
25
+ Number.isFinite(Date.parse(value))
26
+ ? new Date(value).toISOString()
27
+ : undefined;
28
+ }
29
+ function retryAfter(value) {
30
+ if (typeof value === 'number' || (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value))) {
31
+ const delayMs = Number(value) * 1000;
32
+ return Number.isFinite(delayMs) && delayMs >= 0 && delayMs <= Number.MAX_SAFE_INTEGER
33
+ ? { kind: 'delay', delayMs }
34
+ : undefined;
35
+ }
36
+ const at = timestamp(value);
37
+ return at ? { kind: 'date', at } : undefined;
38
+ }
39
+ function rejected(kind) {
40
+ return kind === 'oauth'
41
+ ? 'oauth_reauthentication_required'
42
+ : kind === 'api-key'
43
+ ? 'api_credentials_rejected'
44
+ : 'authentication_rejected';
45
+ }
46
+ function classifyCode(code, kind) {
47
+ switch (code.toLowerCase().replace(/[^a-z0-9]/g, '')) {
48
+ case 'missingcredentials':
49
+ case 'notloggedin':
50
+ case 'signedout':
51
+ return 'missing_credentials';
52
+ case 'invalidapikey':
53
+ case 'apikeyrejected':
54
+ return 'api_credentials_rejected';
55
+ case 'oauthtokenexpired':
56
+ case 'refreshtokenexpired':
57
+ case 'refreshtokenreused':
58
+ case 'invalidgrant':
59
+ return 'oauth_reauthentication_required';
60
+ case 'authenticationerror':
61
+ case 'autherror':
62
+ case 'invalidtoken':
63
+ case 'tokenexpired':
64
+ case 'unauthorized':
65
+ return rejected(kind);
66
+ case 'threadnotfound':
67
+ case 'invalidthreadid':
68
+ case 'conversationnotfound':
69
+ case 'invalidconversation':
70
+ case 'conversationexpired':
71
+ case 'invalidsessionid':
72
+ return 'invalid_provider_session';
73
+ case 'executionpolicydenied':
74
+ case 'sandboxdenied':
75
+ case 'approvaldenied':
76
+ return 'execution_policy_denied';
77
+ case 'ratelimiterror':
78
+ case 'ratelimitexceeded':
79
+ case 'toomanyrequests':
80
+ return 'rate_limited';
81
+ case 'overloadederror':
82
+ case 'serveroverloaded':
83
+ return 'overloaded';
84
+ case 'serviceunavailable':
85
+ case 'internalservererror':
86
+ case 'servererror':
87
+ return 'service_unavailable';
88
+ case 'timeouterror':
89
+ case 'apiconnectiontimeouterror':
90
+ case 'etimedout':
91
+ case 'underrconnecttimeout':
92
+ case 'underrheaderstimeout':
93
+ case 'deadlineexceeded':
94
+ return 'timeout';
95
+ case 'econnreset':
96
+ case 'econnrefused':
97
+ case 'enotfound':
98
+ case 'eaiagain':
99
+ case 'enetunreach':
100
+ case 'ehostunreach':
101
+ case 'apiconnectionerror':
102
+ return 'connection_failed';
103
+ case 'runtimenotfound':
104
+ case 'runtimeunavailable':
105
+ case 'modulenotfound':
106
+ case 'errmodulenotfound':
107
+ return 'runtime_unavailable';
108
+ case 'contextlengthexceeded':
109
+ case 'contextwindowexceeded':
110
+ case 'prompttoolong':
111
+ return 'context_overflow';
112
+ default:
113
+ return undefined;
114
+ }
115
+ }
116
+ function classifyStatus(status, kind) {
117
+ if (status === 401)
118
+ return rejected(kind);
119
+ if (status === 408 || status === 504)
120
+ return 'timeout';
121
+ if (status === 429)
122
+ return 'rate_limited';
123
+ if (status === 529)
124
+ return 'overloaded';
125
+ if (status >= 500)
126
+ return 'service_unavailable';
127
+ return undefined;
128
+ }
129
+ function classifyMessage(message, kind) {
130
+ if (/\b(execution policy|sandbox policy|approval)\b.{0,60}\b(denied|blocked|rejected|not allowed)\b|\bdenied by policy\b/i.test(message))
131
+ return 'execution_policy_denied';
132
+ // Only explicit credential rejection outranks an overlapping conversation error.
133
+ if (/\b(oauth (?:(?:access |refresh )?token|credentials?)|refresh token) (?:is |was |has (?:been )?)?(expired|invalid|rejected|revoked)\b/i.test(message))
134
+ return 'oauth_reauthentication_required';
135
+ if (/\b(thread|conversation|session id)\b.{0,60}\b(not found|invalid|expired|does not exist)\b|\b(invalid|expired) (thread|conversation|session id)\b/i.test(message))
136
+ return 'invalid_provider_session';
137
+ if (/\b(oauth|refresh token)\b.{0,60}\b(expired|invalid|rejected|revoked)\b/i.test(message))
138
+ return 'oauth_reauthentication_required';
139
+ if (/\b(api[ _-]?key)\b.{0,60}\b(invalid|incorrect|rejected|revoked|expired)\b|\b(invalid|incorrect|rejected) api[ _-]?key\b/i.test(message))
140
+ return 'api_credentials_rejected';
141
+ if (/\b(no|missing) (oauth |api[ _-]?key |authentication )?(credentials|token|api[ _-]?key)\b|\bnot (logged|signed) in\b|\bsigned out\b|\bapi[ _-]?key (not configured|not set)\b/i.test(message))
142
+ return 'missing_credentials';
143
+ if (/\b(authentication failed|invalid authentication|credentials (rejected|expired)|token (expired|rejected|invalid)|login (has |is )?expired|please re-?authenticate)\b/i.test(message))
144
+ return rejected(kind);
145
+ if (/\b(context (length|window).{0,40}(exceed|overflow)|prompt (is )?too long|maximum context length)\b/i.test(message))
146
+ return 'context_overflow';
147
+ if (/\b(overloaded|over capacity|server busy)\b/i.test(message))
148
+ return 'overloaded';
149
+ if (/\b(rate[ _-]?limit|too many requests)\b/i.test(message))
150
+ return 'rate_limited';
151
+ if (/\b(timed? ?out|deadline exceeded|etimedout)\b/i.test(message))
152
+ return 'timeout';
153
+ if (/\b(service unavailable|bad gateway|internal server error)\b/i.test(message))
154
+ return 'service_unavailable';
155
+ if (/\b(enotfound|eai_again|econnreset|econnrefused|dns (failure|lookup failed)|fetch failed|network (error|unreachable)|connection (reset|refused|terminated|termination)|upstream connect error|socket hang up)\b/i.test(message))
156
+ return 'connection_failed';
157
+ if (/\b(claude|codex|cli|runtime|executable)\b.{0,60}\b(not found|not installed|unavailable|not executable)\b|\bspawn\s+\S+\s+(ENOENT|EACCES)\b/i.test(message))
158
+ return 'runtime_unavailable';
159
+ return undefined;
160
+ }
161
+ function recovery(code, kind) {
162
+ switch (code) {
163
+ case 'missing_credentials':
164
+ return kind === 'oauth'
165
+ ? 'sign_in'
166
+ : kind === 'api-key'
167
+ ? 'update_credentials'
168
+ : 'check_authentication';
169
+ case 'oauth_reauthentication_required':
170
+ return 'sign_in';
171
+ case 'api_credentials_rejected':
172
+ return 'update_credentials';
173
+ case 'authentication_rejected':
174
+ return 'check_authentication';
175
+ case 'invalid_provider_session':
176
+ return 'start_new_session';
177
+ case 'connection_failed':
178
+ return 'check_connection';
179
+ case 'execution_policy_denied':
180
+ return 'review_execution_policy';
181
+ case 'runtime_unavailable':
182
+ return 'repair_runtime';
183
+ case 'context_overflow':
184
+ return 'reduce_context';
185
+ case 'unknown':
186
+ return 'inspect_error';
187
+ default:
188
+ return 'retry_later';
189
+ }
190
+ }
191
+ /**
192
+ * Classify a caller-designated terminal failure without I/O or recovery actions.
193
+ * Public status and runtime readiness do not establish request authentication.
194
+ */
195
+ function diagnoseProviderFailure(input) {
196
+ const codes = [];
197
+ const messages = [];
198
+ const visited = new Set();
199
+ const queue = [{ value: input.error, depth: 0 }];
200
+ let httpStatus;
201
+ let retry;
202
+ for (let index = 0; index < queue.length && index < 32; index++) {
203
+ const { value, depth } = queue[index];
204
+ if (typeof value === 'string') {
205
+ messages.push(text(value));
206
+ continue;
207
+ }
208
+ if (!value || typeof value !== 'object' || visited.has(value))
209
+ continue;
210
+ visited.add(value);
211
+ for (const key of ['code', 'type', 'name'])
212
+ codes.push(text(read(value, key)));
213
+ messages.push(text(read(value, 'message')));
214
+ for (const key of ['status', 'statusCode', 'httpStatus', 'status_code'])
215
+ httpStatus ??= statusNumber(read(value, key));
216
+ const headers = read(value, 'headers');
217
+ const header = typeof Headers !== 'undefined' && headers instanceof Headers
218
+ ? headers.get('retry-after')
219
+ : (read(headers, 'retry-after') ?? read(headers, 'Retry-After'));
220
+ retry ??= retryAfter(header);
221
+ const delay = read(value, 'retryAfterMs');
222
+ if (!retry &&
223
+ typeof delay === 'number' &&
224
+ Number.isFinite(delay) &&
225
+ delay >= 0 &&
226
+ delay <= Number.MAX_SAFE_INTEGER)
227
+ retry = { kind: 'delay', delayMs: delay };
228
+ // ENOENT by itself can be a missing document. Require a spawn operation.
229
+ if (read(value, 'code') === 'ENOENT' && /^spawn\b/.test(text(read(value, 'syscall'))))
230
+ codes.push('runtime_unavailable');
231
+ if (depth < 5) {
232
+ for (const key of ['error', 'cause', 'response', 'data', 'body']) {
233
+ let nested = read(value, key);
234
+ if (typeof nested === 'string' &&
235
+ nested.length <= 16_384 &&
236
+ nested.trim().startsWith('{')) {
237
+ try {
238
+ nested = JSON.parse(nested);
239
+ }
240
+ catch {
241
+ /* Keep message evidence. */
242
+ }
243
+ }
244
+ if (nested != null)
245
+ queue.push({ value: nested, depth: depth + 1 });
246
+ }
247
+ const errors = read(value, 'errors');
248
+ if (Array.isArray(errors))
249
+ for (const error of errors.slice(0, 8))
250
+ queue.push({ value: error, depth: depth + 1 });
251
+ }
252
+ }
253
+ let authentication;
254
+ const auth = input.authentication;
255
+ const checkedAt = timestamp(auth?.checkedAt);
256
+ if (auth &&
257
+ checkedAt &&
258
+ ['oauth', 'api-key', 'unknown'].includes(auth.credentialKind) &&
259
+ ['authenticated', 'missing', 'signed-out', 'expired', 'rejected', 'unknown'].includes(auth.state) &&
260
+ ['local-check', 'provider-response'].includes(auth.source)) {
261
+ authentication = {
262
+ credentialKind: auth.credentialKind,
263
+ state: auth.state,
264
+ checkedAt,
265
+ source: auth.source,
266
+ };
267
+ }
268
+ let diagnosis = codes
269
+ .map((code) => classifyCode(code, input.credentialKind))
270
+ .find((code) => code !== undefined);
271
+ let source = 'structured-error';
272
+ if (!diagnosis && httpStatus !== undefined) {
273
+ diagnosis = classifyStatus(httpStatus, input.credentialKind);
274
+ source = 'http-status';
275
+ }
276
+ if (!diagnosis) {
277
+ source = 'message';
278
+ for (const message of messages) {
279
+ // Message HTTP codes must have an HTTP/status context, not just a stray number.
280
+ const match = message.match(/\b(?:HTTP(?:\/\d(?:\.\d)?)?(?: error)?|(?:unexpected )?status(?: code)?|API error)\s*[:=]?\s*(\d{3})\b|\b(401|403|429|50[02349]|529)\s+(?:Unauthorized|Forbidden|Too Many Requests|Bad Gateway|Service Unavailable|Internal Server Error|Gateway Timeout)\b/i);
281
+ const status = match ? statusNumber(match[1] ?? match[2]) : undefined;
282
+ // A structured status cannot be replaced by a contradictory status in prose.
283
+ if (httpStatus === undefined && status !== undefined) {
284
+ httpStatus = status;
285
+ diagnosis = classifyStatus(status, input.credentialKind);
286
+ }
287
+ diagnosis ??= classifyMessage(message, input.credentialKind);
288
+ if (diagnosis)
289
+ break;
290
+ }
291
+ }
292
+ if (!diagnosis && authentication && authentication.credentialKind === input.credentialKind) {
293
+ if (authentication.state === 'missing' || authentication.state === 'signed-out')
294
+ diagnosis = 'missing_credentials';
295
+ if (authentication.state === 'expired' || authentication.state === 'rejected')
296
+ diagnosis = rejected(input.credentialKind);
297
+ if (diagnosis)
298
+ source = 'authentication-check';
299
+ }
300
+ diagnosis ??= 'unknown';
301
+ return {
302
+ provider: input.provider,
303
+ credentialKind: input.credentialKind,
304
+ diagnosis,
305
+ recovery: recovery(diagnosis, input.credentialKind),
306
+ evidence: diagnosis === 'unknown'
307
+ ? []
308
+ : [
309
+ {
310
+ source,
311
+ rule: diagnosis,
312
+ ...(source === 'authentication-check'
313
+ ? { checkedAt: authentication.checkedAt }
314
+ : {}),
315
+ },
316
+ ],
317
+ ...(authentication ? { authentication } : {}),
318
+ ...(httpStatus !== undefined ? { httpStatus } : {}),
319
+ ...(retry ? { retryAfter: retry } : {}),
320
+ };
321
+ }
@@ -0,0 +1,8 @@
1
+ import type { AccountProviderId } from './providerIds';
2
+ import type { FetchProviderServiceStatusOptions, ProviderServiceStatus } from './providerServiceStatusTypes';
3
+ /**
4
+ * Fetch public vendor status without credentials, caching, polling, or retries.
5
+ * Incidents supplement a request diagnosis; they do not prove its cause. Consumers
6
+ * own freshness and component/endpoint correlation. Omitted incidents are null.
7
+ */
8
+ export declare function fetchProviderServiceStatus(provider: AccountProviderId, { signal }?: FetchProviderServiceStatusOptions): Promise<ProviderServiceStatus>;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchProviderServiceStatus = fetchProviderServiceStatus;
4
+ const SOURCES = {
5
+ 'claude-code': 'https://status.claude.com/api/v2/summary.json',
6
+ codex: 'https://status.openai.com/api/v2/summary.json',
7
+ };
8
+ function object(value) {
9
+ if (!value || typeof value !== 'object' || Array.isArray(value))
10
+ throw new Error('Invalid object');
11
+ return value;
12
+ }
13
+ function string(value) {
14
+ if (typeof value !== 'string' || !value.trim())
15
+ throw new Error('Invalid string');
16
+ return value;
17
+ }
18
+ function date(value) {
19
+ if (value === undefined || value === null)
20
+ return null;
21
+ const raw = string(value);
22
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(raw) || !Number.isFinite(Date.parse(raw)))
23
+ throw new Error('Invalid date');
24
+ return raw;
25
+ }
26
+ function url(value) {
27
+ if (value === undefined || value === null)
28
+ return null;
29
+ const raw = string(value);
30
+ const parsed = new URL(raw);
31
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
32
+ throw new Error('Invalid link');
33
+ return raw;
34
+ }
35
+ function array(value) {
36
+ if (!Array.isArray(value))
37
+ throw new Error('Invalid array');
38
+ return value;
39
+ }
40
+ function parseSummary(raw) {
41
+ const data = object(raw);
42
+ const status = object(data.status);
43
+ const severity = string(status.indicator);
44
+ if (!['none', 'minor', 'major', 'critical', 'maintenance'].includes(severity))
45
+ throw new Error('Invalid severity');
46
+ const components = array(data.components).map((rawComponent) => {
47
+ const component = object(rawComponent);
48
+ return {
49
+ id: string(component.id),
50
+ name: string(component.name),
51
+ status: string(component.status),
52
+ };
53
+ });
54
+ const incidents = data.incidents === undefined
55
+ ? null
56
+ : array(data.incidents)
57
+ .map((rawIncident) => {
58
+ const incident = object(rawIncident);
59
+ // Statuspage uses objects; its compatible feeds may supply component IDs directly.
60
+ const associations = incident.components ?? incident.component_ids;
61
+ return {
62
+ id: string(incident.id),
63
+ title: string(incident.name),
64
+ status: string(incident.status),
65
+ impact: string(incident.impact),
66
+ url: url(incident.shortlink ?? incident.url),
67
+ updatedAt: date(incident.updated_at),
68
+ componentIds: associations === undefined
69
+ ? null
70
+ : array(associations).map((component) => typeof component === 'string'
71
+ ? string(component)
72
+ : string(object(component).id)),
73
+ };
74
+ })
75
+ .filter((incident) => !['resolved', 'postmortem'].includes(incident.status.toLowerCase()));
76
+ return {
77
+ availability: 'observed',
78
+ severity: severity,
79
+ description: string(status.description),
80
+ providerUpdatedAt: date(object(data.page).updated_at),
81
+ components,
82
+ incidents,
83
+ };
84
+ }
85
+ /**
86
+ * Fetch public vendor status without credentials, caching, polling, or retries.
87
+ * Incidents supplement a request diagnosis; they do not prove its cause. Consumers
88
+ * own freshness and component/endpoint correlation. Omitted incidents are null.
89
+ */
90
+ async function fetchProviderServiceStatus(provider, { signal } = {}) {
91
+ const sourceUrl = SOURCES[provider];
92
+ const observation = () => ({ provider, sourceUrl, checkedAt: new Date().toISOString() });
93
+ const unavailable = (reason, httpStatus) => ({
94
+ ...observation(),
95
+ availability: 'unavailable',
96
+ reason,
97
+ ...(httpStatus !== undefined ? { httpStatus } : {}),
98
+ });
99
+ if (signal?.aborted)
100
+ return unavailable('cancelled');
101
+ const controller = new AbortController();
102
+ let aborted;
103
+ let rejectAbort = () => { };
104
+ const abortPromise = new Promise((_resolve, reject) => {
105
+ rejectAbort = reject;
106
+ });
107
+ const abort = (reason) => {
108
+ if (aborted)
109
+ return;
110
+ aborted = reason;
111
+ controller.abort();
112
+ rejectAbort(new Error(reason));
113
+ };
114
+ const onCancel = () => abort('cancelled');
115
+ signal?.addEventListener('abort', onCancel, { once: true });
116
+ const timer = setTimeout(() => abort('timeout'), 10_000);
117
+ timer.unref?.();
118
+ try {
119
+ return await Promise.race([
120
+ abortPromise,
121
+ (async () => {
122
+ const response = await fetch(sourceUrl, { signal: controller.signal, credentials: 'omit' });
123
+ if (!response.ok)
124
+ return unavailable('http_error', response.status);
125
+ let body;
126
+ try {
127
+ body = await response.json();
128
+ }
129
+ catch (error) {
130
+ return unavailable(aborted ?? (error instanceof SyntaxError ? 'invalid_response' : 'network_error'));
131
+ }
132
+ try {
133
+ return { ...observation(), ...parseSummary(body) };
134
+ }
135
+ catch {
136
+ return unavailable('invalid_response');
137
+ }
138
+ })(),
139
+ ]);
140
+ }
141
+ catch {
142
+ return unavailable(aborted ?? 'network_error');
143
+ }
144
+ finally {
145
+ clearTimeout(timer);
146
+ signal?.removeEventListener('abort', onCancel);
147
+ }
148
+ }
@@ -0,0 +1,43 @@
1
+ import type { AccountProviderId } from './providerIds';
2
+ export type ProviderServiceSeverity = 'none' | 'minor' | 'major' | 'critical' | 'maintenance';
3
+ export interface ProviderServiceComponent {
4
+ id: string;
5
+ name: string;
6
+ status: string;
7
+ }
8
+ export interface ProviderServiceIncident {
9
+ id: string;
10
+ title: string;
11
+ status: string;
12
+ impact: string;
13
+ url: string | null;
14
+ updatedAt: string | null;
15
+ /** null means associations were not supplied; [] means explicitly no associations. */
16
+ componentIds: string[] | null;
17
+ }
18
+ export type ProviderServiceStatusFailureReason = 'http_error' | 'network_error' | 'timeout' | 'cancelled' | 'invalid_response';
19
+ interface ProviderServiceObservation {
20
+ provider: AccountProviderId;
21
+ /** Completion time of this check, independent of the provider's update time. */
22
+ checkedAt: string;
23
+ sourceUrl: string;
24
+ }
25
+ export interface ObservedProviderServiceStatus extends ProviderServiceObservation {
26
+ availability: 'observed';
27
+ severity: ProviderServiceSeverity;
28
+ description: string;
29
+ providerUpdatedAt: string | null;
30
+ components: ProviderServiceComponent[];
31
+ /** null means the feed omitted incident evidence, not that there are no incidents. */
32
+ incidents: ProviderServiceIncident[] | null;
33
+ }
34
+ export interface UnavailableProviderServiceStatus extends ProviderServiceObservation {
35
+ availability: 'unavailable';
36
+ reason: ProviderServiceStatusFailureReason;
37
+ httpStatus?: number;
38
+ }
39
+ export type ProviderServiceStatus = ObservedProviderServiceStatus | UnavailableProviderServiceStatus;
40
+ export interface FetchProviderServiceStatusOptions {
41
+ signal?: AbortSignal;
42
+ }
43
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -4,6 +4,7 @@
4
4
  * Supports both status.claude.com and status.openai.com (same API format).
5
5
  * Polling / eventing is the caller's responsibility.
6
6
  */
7
+ /** Legacy projection. Use ProviderServiceStatus for explicit availability evidence. */
7
8
  export interface ProviderStatusState {
8
9
  indicator: 'none' | 'minor' | 'major' | 'critical';
9
10
  description: string;
@@ -19,15 +20,19 @@ export interface ProviderStatusState {
19
20
  } | null;
20
21
  updatedAt: string;
21
22
  }
23
+ /** Compatibility projection only; it cannot express evidence availability. */
24
+ export declare function toLegacyProviderStatus(status: import('./providerServiceStatusTypes').ProviderServiceStatus): ProviderStatusState;
22
25
  /**
23
26
  * Fetch current Claude API status from status.claude.com.
24
27
  *
25
28
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
29
+ * Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
26
30
  */
27
31
  export declare function fetchProviderStatus(): Promise<ProviderStatusState>;
28
32
  /**
29
33
  * Fetch current OpenAI API status from status.openai.com.
30
34
  *
31
35
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
36
+ * Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
32
37
  */
33
38
  export declare function fetchOpenAIStatus(): Promise<ProviderStatusState>;
@@ -6,8 +6,38 @@
6
6
  * Polling / eventing is the caller's responsibility.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.toLegacyProviderStatus = toLegacyProviderStatus;
9
10
  exports.fetchProviderStatus = fetchProviderStatus;
10
11
  exports.fetchOpenAIStatus = fetchOpenAIStatus;
12
+ /** Compatibility projection only; it cannot express evidence availability. */
13
+ function toLegacyProviderStatus(status) {
14
+ if (status.availability === 'unavailable') {
15
+ return {
16
+ indicator: 'none',
17
+ description: 'Status unavailable',
18
+ affectedComponents: [],
19
+ activeIncident: null,
20
+ updatedAt: status.checkedAt,
21
+ };
22
+ }
23
+ const incident = status.incidents?.[0];
24
+ return {
25
+ indicator: status.severity === 'maintenance' ? 'minor' : status.severity,
26
+ description: status.description,
27
+ affectedComponents: status.components
28
+ .filter((component) => component.status !== 'operational')
29
+ .map(({ name, status }) => ({ name, status })),
30
+ activeIncident: incident
31
+ ? {
32
+ name: incident.title,
33
+ impact: incident.impact,
34
+ shortlink: incident.url ?? '',
35
+ updatedAt: incident.updatedAt ?? '',
36
+ }
37
+ : null,
38
+ updatedAt: status.providerUpdatedAt ?? '',
39
+ };
40
+ }
11
41
  const CLAUDE_BASE = 'https://status.claude.com';
12
42
  const OPENAI_BASE = 'https://status.openai.com';
13
43
  function fallbackState() {
@@ -76,6 +106,7 @@ async function fetchWithTimeout(url) {
76
106
  * Fetch current Claude API status from status.claude.com.
77
107
  *
78
108
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
109
+ * Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
79
110
  */
80
111
  async function fetchProviderStatus() {
81
112
  return fetchStatusPage(CLAUDE_BASE);
@@ -84,6 +115,7 @@ async function fetchProviderStatus() {
84
115
  * Fetch current OpenAI API status from status.openai.com.
85
116
  *
86
117
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
118
+ * Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
87
119
  */
88
120
  async function fetchOpenAIStatus() {
89
121
  return fetchStatusPage(OPENAI_BASE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.26.2",
3
+ "version": "0.26.4",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "keywords": [
6
6
  "claude-code",