pi-commandcode-provider 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ import type {
2
+ CommandCodeCredits,
3
+ CommandCodeQuota,
4
+ CommandCodeSubscription,
5
+ CommandCodeWindowLimit,
6
+ } from "./quota-types.ts"
7
+
8
+ export function formatWindowLimits(
9
+ limits: readonly CommandCodeWindowLimit[],
10
+ now: () => number = Date.now,
11
+ ): string[] {
12
+ const labels: Record<CommandCodeWindowLimit["window"], string> = {
13
+ fiveHour: "5-hour",
14
+ weekly: "Weekly",
15
+ }
16
+
17
+ return limits.map((limit) => {
18
+ const used = limit.used.toFixed(2)
19
+ const cap = limit.cap.toFixed(2)
20
+ const percent = limit.cap > 0 ? Math.round((limit.used / limit.cap) * 100) : 0
21
+ const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`
22
+ return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`
23
+ })
24
+ }
25
+
26
+ function formatResetClock(resetAtSeconds: number, now: () => number): string {
27
+ const date = new Date(resetAtSeconds * 1000)
28
+ if (Number.isNaN(date.getTime())) return "unknown"
29
+ const diffMs = date.getTime() - now()
30
+ if (diffMs <= 0) return "soon"
31
+ const minutes = Math.ceil(diffMs / 60_000)
32
+ if (minutes < 60) return `in ${minutes}m`
33
+ const hours = Math.floor(minutes / 60)
34
+ const remainingMinutes = minutes % 60
35
+ if (hours < 24) {
36
+ return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`
37
+ }
38
+ const days = Math.floor(hours / 24)
39
+ return days === 1 ? "in 1 day" : `in ${days} days`
40
+ }
41
+
42
+ function creditsDetail(credits: CommandCodeCredits | null): string | undefined {
43
+ if (!credits) return undefined
44
+ const parts = [
45
+ `monthly $${credits.monthlyCredits.toFixed(2)}`,
46
+ `purchased $${credits.purchasedCredits.toFixed(2)}`,
47
+ ]
48
+ if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`)
49
+ return `Sources: ${parts.join(" / ")}`
50
+ }
51
+
52
+ function parsePeriodEnd(value: string): Date | null {
53
+ const trimmed = value.trim()
54
+ const timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
55
+ if (!Number.isFinite(timestamp) || timestamp < 0) return null
56
+ const milliseconds = timestamp >= 1e12 ? timestamp : timestamp * 1000
57
+ const date = new Date(milliseconds)
58
+ return Number.isNaN(date.getTime()) ? null : date
59
+ }
60
+
61
+ function subscriptionLine(
62
+ subscription: CommandCodeSubscription,
63
+ now: () => number = Date.now,
64
+ ): string {
65
+ const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim()
66
+ const status = subscription.status ? ` (${subscription.status})` : ""
67
+ let renewal = ""
68
+ if (subscription.currentPeriodEnd) {
69
+ const end = parsePeriodEnd(subscription.currentPeriodEnd)
70
+ if (end) {
71
+ const diffMs = end.getTime() - now()
72
+ const days = Math.ceil(diffMs / 86_400_000)
73
+ const dateStr = end.toLocaleDateString("en-US", {
74
+ month: "short",
75
+ day: "numeric",
76
+ timeZone: "UTC",
77
+ })
78
+ if (days > 0) {
79
+ renewal = ` · renews ${dateStr} (${days}d)`
80
+ } else if (days === 0) {
81
+ renewal = ` · renews ${dateStr} (today)`
82
+ } else {
83
+ renewal = ` · renewed ${dateStr}`
84
+ }
85
+ }
86
+ }
87
+ return `Plan: ${plan}${status}${renewal}`
88
+ }
89
+
90
+ function formatTokens(tokens: number): string {
91
+ if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B`
92
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
93
+ if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`
94
+ return String(tokens)
95
+ }
96
+
97
+ export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.now): string {
98
+ const lines: string[] = []
99
+ const remaining = quota.credits?.remainingCredits ?? 0
100
+ const spent = quota.summary?.totalCost ?? 0
101
+ const pool = remaining + spent
102
+
103
+ if (quota.credits || quota.summary) {
104
+ lines.push("Credits")
105
+ lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`)
106
+ lines.push(` Used: $${spent.toFixed(2)}`)
107
+ lines.push(` ${pool > 0 ? Math.round((spent / pool) * 100) : 0}% used`)
108
+ }
109
+
110
+ const detail = creditsDetail(quota.credits)
111
+ if (detail) lines.push(detail)
112
+ if (quota.subscription) lines.push(subscriptionLine(quota.subscription, now))
113
+
114
+ if (quota.summary) {
115
+ lines.push("")
116
+ lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage")
117
+ lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`)
118
+ lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`)
119
+ if (quota.summary.totalTokens !== undefined) {
120
+ lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`)
121
+ }
122
+ }
123
+
124
+ lines.push("")
125
+ lines.push("Account")
126
+ lines.push(` ${quota.account.keyName ?? quota.account.login}`)
127
+
128
+ const limits = quota.credits?.windowLimits ?? []
129
+ if (limits.length > 0) {
130
+ lines.push("")
131
+ lines.push("Usage windows:")
132
+ lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`))
133
+ }
134
+
135
+ if ((quota.unavailable?.length ?? 0) > 0) {
136
+ lines.push("")
137
+ lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`)
138
+ }
139
+
140
+ lines.push("")
141
+ lines.push("Full detail: https://commandcode.ai/usage")
142
+ return lines.join("\n")
143
+ }
@@ -0,0 +1,47 @@
1
+ export interface CommandCodeWindowLimit {
2
+ window: "fiveHour" | "weekly"
3
+ used: number
4
+ cap: number
5
+ resetAt: number | null
6
+ }
7
+
8
+ export interface CommandCodeCredits {
9
+ monthlyCredits: number
10
+ purchasedCredits: number
11
+ freeCredits: number
12
+ remainingCredits: number
13
+ windowLimits: CommandCodeWindowLimit[]
14
+ }
15
+
16
+ export interface CommandCodeSubscription {
17
+ planId: string | null
18
+ status: string | null
19
+ currentPeriodStart: string | null
20
+ currentPeriodEnd: string | null
21
+ }
22
+
23
+ export interface CommandCodeUsageSummary {
24
+ totalCost: number
25
+ totalCount: number
26
+ totalTokens?: number
27
+ }
28
+
29
+ export type CommandCodeQuotaSection = "credits" | "subscription" | "usage"
30
+
31
+ export interface CommandCodeQuota {
32
+ account: {
33
+ login: string
34
+ orgId: string | null
35
+ keyName?: string
36
+ }
37
+ credits: CommandCodeCredits | null
38
+ subscription: CommandCodeSubscription | null
39
+ summary: CommandCodeUsageSummary | null
40
+ unavailable?: readonly CommandCodeQuotaSection[]
41
+ }
42
+
43
+ export type CommandCodeQuotaErrorKind = "config" | "http" | "network" | "timeout"
44
+
45
+ export type CommandCodeQuotaResult =
46
+ | { ok: true; quota: CommandCodeQuota }
47
+ | { ok: false; error: { message: string; kind: CommandCodeQuotaErrorKind } }
package/src/quota.ts ADDED
@@ -0,0 +1,342 @@
1
+ import { redactCommandCodeErrorText } from "./overflow.ts"
2
+ import type {
3
+ CommandCodeCredits,
4
+ CommandCodeQuotaResult,
5
+ CommandCodeQuotaSection,
6
+ CommandCodeSubscription,
7
+ CommandCodeUsageSummary,
8
+ CommandCodeWindowLimit,
9
+ } from "./quota-types.ts"
10
+
11
+ export const DEFAULT_API_BASE = "https://api.commandcode.ai"
12
+ export const QUOTA_TIMEOUT_MS = 15_000
13
+
14
+ interface FetchOptions {
15
+ apiKey: string
16
+ baseUrl?: string
17
+ fetchImpl?: typeof fetch
18
+ timeoutMs?: number
19
+ extraHeaders?: Record<string, string>
20
+ }
21
+
22
+ interface HttpErrorShape {
23
+ __httpError: true
24
+ message: string
25
+ status: number
26
+ body: string
27
+ }
28
+
29
+ interface QuotaErrorShape {
30
+ __quotaError: true
31
+ kind: "timeout" | "network"
32
+ }
33
+
34
+ function isRecord(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value)
36
+ }
37
+
38
+ function numberValue(value: unknown): number | undefined {
39
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
40
+ }
41
+
42
+ function stringValue(value: unknown): string | undefined {
43
+ return typeof value === "string" && value.length > 0 ? value : undefined
44
+ }
45
+
46
+ function timestampValue(value: unknown): string | undefined {
47
+ const text = stringValue(value)
48
+ if (text) return text
49
+ const number = numberValue(value)
50
+ return number === undefined ? undefined : String(number)
51
+ }
52
+
53
+ function errorMessage(error: unknown): string {
54
+ return error instanceof Error ? error.message : String(error)
55
+ }
56
+
57
+ function normalizeResetAt(value: unknown): number | null {
58
+ let timestamp: number | undefined
59
+ if (typeof value === "number" && Number.isFinite(value)) timestamp = value
60
+ if (typeof value === "string" && value.length > 0) {
61
+ const trimmed = value.trim()
62
+ timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
63
+ }
64
+ if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return null
65
+ return timestamp >= 1e12 ? Math.round(timestamp / 1000) : timestamp
66
+ }
67
+
68
+ export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
69
+ if (!isRecord(value)) return []
70
+ const limits: CommandCodeWindowLimit[] = []
71
+ for (const [window, entry] of [
72
+ ["fiveHour", value.fiveHour],
73
+ ["weekly", value.weekly],
74
+ ] as const) {
75
+ if (!isRecord(entry)) continue
76
+ const used = numberValue(entry.used)
77
+ const cap = numberValue(entry.cap)
78
+ if (used === undefined || cap === undefined || (used === 0 && cap === 0)) continue
79
+ limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) })
80
+ }
81
+ return limits
82
+ }
83
+
84
+ function parseCredits(value: unknown): CommandCodeCredits | null {
85
+ if (!isRecord(value) || !isRecord(value.credits)) return null
86
+ const credits = value.credits
87
+ const monthlyCredits = numberValue(credits.monthlyCredits)
88
+ const purchasedCredits = numberValue(credits.purchasedCredits)
89
+ const freeCredits = numberValue(credits.freeCredits)
90
+ if (monthlyCredits === undefined && purchasedCredits === undefined && freeCredits === undefined) {
91
+ return null
92
+ }
93
+ const monthly = monthlyCredits ?? 0
94
+ const purchased = purchasedCredits ?? 0
95
+ const free = freeCredits ?? 0
96
+ return {
97
+ monthlyCredits: monthly,
98
+ purchasedCredits: purchased,
99
+ freeCredits: free,
100
+ remainingCredits: monthly + purchased + free,
101
+ windowLimits: windowLimitsFromCredits(value.windowLimits),
102
+ }
103
+ }
104
+
105
+ function parseSubscription(value: unknown): CommandCodeSubscription | null {
106
+ if (!isRecord(value) || !isRecord(value.data)) return null
107
+ const data = value.data
108
+ const planId = stringValue(data.planId)
109
+ const status = stringValue(data.status)
110
+ const currentPeriodStart = timestampValue(data.currentPeriodStart)
111
+ const currentPeriodEnd = timestampValue(data.currentPeriodEnd)
112
+ if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null
113
+ return {
114
+ planId: planId ?? null,
115
+ status: status ?? null,
116
+ currentPeriodStart: currentPeriodStart ?? null,
117
+ currentPeriodEnd: currentPeriodEnd ?? null,
118
+ }
119
+ }
120
+
121
+ function parseSummary(value: unknown): CommandCodeUsageSummary | null {
122
+ if (!isRecord(value)) return null
123
+ const totalCost = numberValue(value.totalCost)
124
+ const totalCount = numberValue(value.totalCount)
125
+ if (totalCost === undefined || totalCount === undefined) return null
126
+ const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens)
127
+ return { totalCost, totalCount, ...(totalTokens === undefined ? {} : { totalTokens }) }
128
+ }
129
+
130
+ function parseWhoami(value: unknown): {
131
+ login: string
132
+ orgId: string | null
133
+ keyName?: string
134
+ } | null {
135
+ if (!isRecord(value)) return null
136
+ const org = isRecord(value.org) ? value.org : undefined
137
+ const user = isRecord(value.user) ? value.user : undefined
138
+ const login =
139
+ (org ? stringValue(org.login) : undefined) ??
140
+ (user ? (stringValue(user.userName) ?? stringValue(user.name)) : undefined)
141
+ if (!login) return null
142
+ const orgId = org ? stringValue(org.id) : undefined
143
+ const keyName = user ? (stringValue(user.keyName) ?? stringValue(user.displayName)) : undefined
144
+ return { login, orgId: orgId ?? null, ...(keyName ? { keyName } : {}) }
145
+ }
146
+
147
+ function buildUrl(path: string, params: Record<string, string | undefined>): string {
148
+ const search = new URLSearchParams()
149
+ for (const [key, value] of Object.entries(params)) {
150
+ if (value) search.set(key, value)
151
+ }
152
+ const query = search.toString()
153
+ return `${path}${query ? `?${query}` : ""}`
154
+ }
155
+
156
+ function isHttpError(value: unknown): value is HttpErrorShape {
157
+ return (
158
+ isRecord(value) &&
159
+ value.__httpError === true &&
160
+ typeof value.message === "string" &&
161
+ typeof value.status === "number" &&
162
+ typeof value.body === "string"
163
+ )
164
+ }
165
+
166
+ function isQuotaError(value: unknown): value is QuotaErrorShape {
167
+ return (
168
+ isRecord(value) &&
169
+ value.__quotaError === true &&
170
+ (value.kind === "timeout" || value.kind === "network")
171
+ )
172
+ }
173
+
174
+ function isBlockingHttpError(error: HttpErrorShape): boolean {
175
+ return error.status === 401 || error.status === 403
176
+ }
177
+
178
+ function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult {
179
+ const detail = error.body.trim().slice(0, 200)
180
+ return {
181
+ ok: false,
182
+ error: {
183
+ kind: "http",
184
+ message: redactValue(
185
+ `${context} request failed (${error.status}): ${detail || error.message}`,
186
+ ),
187
+ },
188
+ }
189
+ }
190
+
191
+ class QuotaTimeoutError extends Error {}
192
+
193
+ export async function fetchCommandCodeQuota(
194
+ options: FetchOptions,
195
+ ): Promise<CommandCodeQuotaResult> {
196
+ if (!options.apiKey) {
197
+ return { ok: false, error: { message: "No Command Code API key found", kind: "config" } }
198
+ }
199
+
200
+ const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
201
+ const fetchImpl = options.fetchImpl ?? fetch
202
+ const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
203
+ const overallController = new AbortController()
204
+ const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
205
+ const headers = {
206
+ accept: "application/json",
207
+ Authorization: `Bearer ${options.apiKey}`,
208
+ ...options.extraHeaders,
209
+ }
210
+
211
+ const request = async (path: string): Promise<unknown> => {
212
+ if (overallController.signal.aborted) throw new QuotaTimeoutError()
213
+ try {
214
+ const response = await fetchImpl(`${baseUrl}${path}`, {
215
+ method: "GET",
216
+ headers,
217
+ signal: overallController.signal,
218
+ })
219
+ if (!response.ok) {
220
+ return {
221
+ __httpError: true,
222
+ message:
223
+ response.status === 401 || response.status === 403
224
+ ? "Command Code rejected the API key"
225
+ : response.statusText,
226
+ status: response.status,
227
+ body: await response.text().catch(() => ""),
228
+ } satisfies HttpErrorShape
229
+ }
230
+ return await response.json()
231
+ } catch (error) {
232
+ if (overallController.signal.aborted) throw new QuotaTimeoutError()
233
+ throw error
234
+ }
235
+ }
236
+
237
+ const safeRequest = async (path: string): Promise<unknown> => {
238
+ try {
239
+ return await request(path)
240
+ } catch (error) {
241
+ return {
242
+ __quotaError: true,
243
+ kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
244
+ } satisfies QuotaErrorShape
245
+ }
246
+ }
247
+
248
+ try {
249
+ const whoamiRaw = await request("/alpha/whoami")
250
+ if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami")
251
+ const account = parseWhoami(whoamiRaw)
252
+ if (!account) {
253
+ return {
254
+ ok: false,
255
+ error: { kind: "http", message: "Command Code returned an unrecognized account response" },
256
+ }
257
+ }
258
+
259
+ const orgId = account.orgId ?? undefined
260
+ const [creditsRaw, subscriptionRaw] = await Promise.all([
261
+ safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
262
+ safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId })),
263
+ ])
264
+ if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
265
+ return httpFailure(creditsRaw, "credits")
266
+ }
267
+ if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
268
+ return httpFailure(subscriptionRaw, "subscription")
269
+ }
270
+
271
+ const unavailable: CommandCodeQuotaSection[] = []
272
+ const credits =
273
+ isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw)
274
+ if (!credits) unavailable.push("credits")
275
+ const subscription =
276
+ isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw)
277
+ ? null
278
+ : parseSubscription(subscriptionRaw)
279
+ if (!subscription) unavailable.push("subscription")
280
+
281
+ const summaryRaw = await safeRequest(
282
+ buildUrl("/alpha/usage/summary", {
283
+ orgId,
284
+ since: subscription?.currentPeriodStart ?? undefined,
285
+ }),
286
+ )
287
+ if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
288
+ return httpFailure(summaryRaw, "summary")
289
+ }
290
+ const summary =
291
+ isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw)
292
+ if (!summary) unavailable.push("usage")
293
+
294
+ if (!credits && !subscription && !summary) {
295
+ return {
296
+ ok: false,
297
+ error: {
298
+ kind: overallController.signal.aborted ? "timeout" : "http",
299
+ message: overallController.signal.aborted
300
+ ? "Command Code quota request timed out"
301
+ : "Command Code returned no recognized usage data for the account",
302
+ },
303
+ }
304
+ }
305
+
306
+ return {
307
+ ok: true,
308
+ quota: {
309
+ account,
310
+ credits,
311
+ subscription,
312
+ summary,
313
+ ...(unavailable.length > 0 ? { unavailable } : {}),
314
+ },
315
+ }
316
+ } catch (error) {
317
+ if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
318
+ return {
319
+ ok: false,
320
+ error: { message: "Command Code quota request timed out", kind: "timeout" },
321
+ }
322
+ }
323
+ return {
324
+ ok: false,
325
+ error: {
326
+ message: redactValue(`Failed to fetch Command Code quota: ${errorMessage(error)}`),
327
+ kind: "network",
328
+ },
329
+ }
330
+ } finally {
331
+ clearTimeout(overallTimer)
332
+ }
333
+ }
334
+
335
+ export function redactValue(value: string): string {
336
+ return redactCommandCodeErrorText(value)
337
+ .replace(
338
+ /("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi,
339
+ "$1[redacted]",
340
+ )
341
+ .trim()
342
+ }
package/src/runtime.ts CHANGED
@@ -26,13 +26,17 @@ export interface CommandCodeRuntimeApi<
26
26
  export interface CommandCodeRuntimeOptions<TProviderConfig> {
27
27
  endpoint: string
28
28
  cachePath: string
29
- loadModels: () => Promise<LoadCommandCodeModelsResult>
29
+ loadModels: (signal: AbortSignal) => Promise<LoadCommandCodeModelsResult>
30
+ /** Cached catalog only; resolves to an empty list when no valid cache exists. */
31
+ loadCachedModels: () => Promise<readonly CommandCodeModel[]>
30
32
  createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
33
+ getTransport?: () => "unknown" | "provider" | "generate"
31
34
  now?: () => number
32
35
  logWarning?: (message: string) => void
33
36
  }
34
37
 
35
38
  export interface CommandCodeRuntimeStatus {
39
+ transport: "unknown" | "provider" | "generate"
36
40
  source: LoadCommandCodeModelsResult["source"]
37
41
  modelCount: number
38
42
  lastSuccess?: number
@@ -86,6 +90,7 @@ function formatTimestamp(timestamp: number | undefined): string {
86
90
 
87
91
  export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
88
92
  const lines = [
93
+ `transport: ${status.transport}`,
89
94
  `source: ${status.source}`,
90
95
  `model count: ${status.modelCount}`,
91
96
  `last success: ${formatTimestamp(status.lastSuccess)}`,
@@ -105,6 +110,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
105
110
  private status: CommandCodeRuntimeStatus
106
111
  private providerRegistered = false
107
112
  private refreshPromise: Promise<CommandCodeRefreshResult> | undefined
113
+ private readonly shutdown = new AbortController()
108
114
 
109
115
  constructor(
110
116
  private readonly pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
@@ -113,6 +119,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
113
119
  this.now = options.now ?? Date.now
114
120
  this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
115
121
  const initialStatus: CommandCodeRuntimeStatus = {
122
+ transport: "unknown",
116
123
  source: "empty",
117
124
  modelCount: 0,
118
125
  cachePath: options.cachePath,
@@ -123,12 +130,40 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
123
130
  }
124
131
 
125
132
  getStatus(): CommandCodeRuntimeStatus {
126
- return { ...this.status }
133
+ return {
134
+ ...this.status,
135
+ transport: this.options.getTransport?.() ?? "unknown",
136
+ }
127
137
  }
128
138
 
139
+ /**
140
+ * Registers the cached catalog immediately and refreshes it in the
141
+ * background so host startup does not wait for the network. Without a
142
+ * valid cache the live refresh is awaited so models are available at once.
143
+ */
129
144
  async initialize(): Promise<void> {
130
145
  this.registerCommands()
131
- await this.refresh()
146
+
147
+ const cached = await this.options.loadCachedModels()
148
+ if (cached.length === 0) {
149
+ await this.refresh()
150
+ return
151
+ }
152
+
153
+ this.pi.registerProvider("commandcode", this.options.createProviderConfig(cached))
154
+ this.providerRegistered = true
155
+ this.status = {
156
+ ...this.status,
157
+ source: "cache",
158
+ modelCount: cached.length,
159
+ lastSuccess: this.now(),
160
+ }
161
+ void this.refresh()
162
+ }
163
+
164
+ /** Aborts any background refresh so a stopping host does not wait for the network. */
165
+ dispose(): void {
166
+ this.shutdown.abort(new Error("Command Code provider shut down"))
132
167
  }
133
168
 
134
169
  refresh(): Promise<CommandCodeRefreshResult> {
@@ -149,7 +184,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
149
184
  }
150
185
 
151
186
  try {
152
- const loaded = await this.options.loadModels()
187
+ const loaded = await this.options.loadModels(this.shutdown.signal)
153
188
  const warning = loaded.warning ? redactDiagnosticText(loaded.warning) : undefined
154
189
 
155
190
  const shouldRegister =
@@ -210,6 +245,14 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
210
245
  warning: preservedWarning,
211
246
  }
212
247
  } catch (error) {
248
+ if (this.shutdown.signal.aborted) {
249
+ this.status = { ...this.status, refreshing: false }
250
+ return {
251
+ refreshed: false,
252
+ source: this.status.source,
253
+ modelCount: this.status.modelCount,
254
+ }
255
+ }
213
256
  const warning = redactDiagnosticText(
214
257
  `Could not refresh the Command Code model catalog: ${errorMessage(error)}`,
215
258
  )
@@ -259,10 +302,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
259
302
  this.pi.registerCommand("commandcode-status", {
260
303
  description: "Show redacted Command Code provider diagnostics",
261
304
  handler: async (_args, ctx) => {
262
- ctx.ui.notify(
263
- formatCommandCodeStatus(this.status),
264
- this.status.warning ? "warning" : "info",
265
- )
305
+ const status = this.getStatus()
306
+ ctx.ui.notify(formatCommandCodeStatus(status), status.warning ? "warning" : "info")
266
307
  },
267
308
  })
268
309
  }