@leo000001/opencode-quota-sidebar 4.0.9 → 4.0.12

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,273 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { debug, isRecord } from "./helpers.js";
4
+ function stripJsonComments(input) {
5
+ let output = "";
6
+ let inString = false;
7
+ let escaping = false;
8
+ let lineComment = false;
9
+ let blockComment = false;
10
+ for (let index = 0; index < input.length; index++) {
11
+ const char = input[index];
12
+ const next = input[index + 1];
13
+ if (lineComment) {
14
+ if (char === "\n") {
15
+ lineComment = false;
16
+ output += char;
17
+ }
18
+ continue;
19
+ }
20
+ if (blockComment) {
21
+ if (char === "*" && next === "/") {
22
+ blockComment = false;
23
+ index += 1;
24
+ }
25
+ else if (char === "\n") {
26
+ output += char;
27
+ }
28
+ continue;
29
+ }
30
+ if (inString) {
31
+ output += char;
32
+ if (escaping) {
33
+ escaping = false;
34
+ }
35
+ else if (char === "\\") {
36
+ escaping = true;
37
+ }
38
+ else if (char === '"') {
39
+ inString = false;
40
+ }
41
+ continue;
42
+ }
43
+ if (char === '"') {
44
+ inString = true;
45
+ output += char;
46
+ continue;
47
+ }
48
+ if (char === "/" && next === "/") {
49
+ lineComment = true;
50
+ index += 1;
51
+ continue;
52
+ }
53
+ if (char === "/" && next === "*") {
54
+ blockComment = true;
55
+ index += 1;
56
+ continue;
57
+ }
58
+ output += char;
59
+ }
60
+ return output;
61
+ }
62
+ function stripTrailingCommas(input) {
63
+ let output = "";
64
+ let inString = false;
65
+ let escaping = false;
66
+ for (let index = 0; index < input.length; index++) {
67
+ const char = input[index];
68
+ if (inString) {
69
+ output += char;
70
+ if (escaping) {
71
+ escaping = false;
72
+ }
73
+ else if (char === "\\") {
74
+ escaping = true;
75
+ }
76
+ else if (char === '"') {
77
+ inString = false;
78
+ }
79
+ continue;
80
+ }
81
+ if (char === '"') {
82
+ inString = true;
83
+ output += char;
84
+ continue;
85
+ }
86
+ if (char !== ",") {
87
+ output += char;
88
+ continue;
89
+ }
90
+ let lookahead = index + 1;
91
+ while (lookahead < input.length && /\s/.test(input[lookahead])) {
92
+ lookahead += 1;
93
+ }
94
+ if (input[lookahead] === "}" || input[lookahead] === "]")
95
+ continue;
96
+ output += char;
97
+ }
98
+ return output;
99
+ }
100
+ export function parseJsonc(text) {
101
+ return JSON.parse(stripTrailingCommas(stripJsonComments(text)));
102
+ }
103
+ function providerEntriesFromCollection(value) {
104
+ const providers = [];
105
+ if (Array.isArray(value)) {
106
+ for (const item of value) {
107
+ if (!isRecord(item))
108
+ continue;
109
+ const providerID = typeof item.id === "string" ? item.id : undefined;
110
+ if (!providerID)
111
+ continue;
112
+ providers.push({ providerKey: providerID, providerID, value: item });
113
+ }
114
+ return providers;
115
+ }
116
+ if (!isRecord(value))
117
+ return providers;
118
+ for (const [providerKey, providerValue] of Object.entries(value)) {
119
+ if (!isRecord(providerValue))
120
+ continue;
121
+ const providerID = typeof providerValue.id === "string" ? providerValue.id : providerKey;
122
+ providers.push({ providerKey, providerID, value: providerValue });
123
+ }
124
+ return providers;
125
+ }
126
+ function mergeRecord(base, next) {
127
+ if (!base)
128
+ return next;
129
+ if (!next)
130
+ return base;
131
+ return {
132
+ ...base,
133
+ ...next,
134
+ };
135
+ }
136
+ function mergeDeepRecord(base, next) {
137
+ if (!base)
138
+ return next;
139
+ if (!next)
140
+ return base;
141
+ const merged = { ...base };
142
+ for (const [key, value] of Object.entries(next)) {
143
+ const existing = merged[key];
144
+ merged[key] =
145
+ isRecord(existing) && isRecord(value)
146
+ ? mergeDeepRecord(existing, value)
147
+ : value;
148
+ }
149
+ return merged;
150
+ }
151
+ function mergeCost(base, next) {
152
+ if (isRecord(base) && isRecord(next)) {
153
+ return mergeDeepRecord(base, next);
154
+ }
155
+ return next ?? base;
156
+ }
157
+ function extractModelsFromProvider(provider) {
158
+ const models = provider.value.models;
159
+ const entries = [];
160
+ const pushModel = (modelKey, value) => {
161
+ const modelID = typeof value.id === "string" ? value.id : modelKey;
162
+ if (!modelID)
163
+ return;
164
+ entries.push({
165
+ providerKey: provider.providerKey,
166
+ providerID: provider.providerID,
167
+ modelID,
168
+ modelKey,
169
+ cost: value.cost,
170
+ options: isRecord(value.options) ? value.options : undefined,
171
+ headers: isRecord(value.headers) ? value.headers : undefined,
172
+ api: isRecord(value.api) ? value.api : undefined,
173
+ limit: isRecord(value.limit) ? value.limit : undefined,
174
+ });
175
+ };
176
+ if (Array.isArray(models)) {
177
+ for (const item of models) {
178
+ if (!isRecord(item))
179
+ continue;
180
+ pushModel(typeof item.id === "string" ? item.id : undefined, item);
181
+ }
182
+ return entries;
183
+ }
184
+ if (!isRecord(models))
185
+ return entries;
186
+ for (const [modelKey, modelValue] of Object.entries(models)) {
187
+ if (!isRecord(modelValue))
188
+ continue;
189
+ pushModel(modelKey, modelValue);
190
+ }
191
+ return entries;
192
+ }
193
+ function mergedModelMapKey(model) {
194
+ return `${model.providerKey || model.providerID}:${model.modelKey || model.modelID}`;
195
+ }
196
+ function mergedProviderID(existing, next) {
197
+ if (next.providerKey &&
198
+ next.providerID === next.providerKey &&
199
+ existing?.providerID) {
200
+ return existing.providerID;
201
+ }
202
+ return (next.providerID || existing?.providerID || next.providerKey || "unknown");
203
+ }
204
+ function mergedModelID(existing, next) {
205
+ if (next.modelKey && next.modelID === next.modelKey && existing?.modelID) {
206
+ return existing.modelID;
207
+ }
208
+ return next.modelID || existing?.modelID || next.modelKey || "unknown";
209
+ }
210
+ export function extractOpenCodePricingModels(config) {
211
+ if (!isRecord(config))
212
+ return [];
213
+ const providers = [
214
+ ...providerEntriesFromCollection(config.provider),
215
+ ...providerEntriesFromCollection(config.providers),
216
+ ];
217
+ const merged = new Map();
218
+ for (const provider of providers) {
219
+ for (const model of extractModelsFromProvider(provider)) {
220
+ const key = mergedModelMapKey(model);
221
+ const existing = merged.get(key);
222
+ merged.set(key, {
223
+ providerKey: model.providerKey ?? existing?.providerKey,
224
+ providerID: mergedProviderID(existing, model),
225
+ modelID: mergedModelID(existing, model),
226
+ modelKey: model.modelKey ?? existing?.modelKey,
227
+ cost: mergeCost(existing?.cost, model.cost),
228
+ options: mergeRecord(existing?.options, model.options),
229
+ headers: mergeRecord(existing?.headers, model.headers),
230
+ api: mergeRecord(existing?.api, model.api),
231
+ limit: mergeRecord(existing?.limit, model.limit),
232
+ });
233
+ }
234
+ }
235
+ return [...merged.values()];
236
+ }
237
+ export async function loadOpenCodePricingModels(paths) {
238
+ const seen = new Set();
239
+ const merged = new Map();
240
+ for (const originalPath of paths) {
241
+ const filePath = path.resolve(originalPath);
242
+ const key = process.platform === "win32" ? filePath.toLowerCase() : filePath;
243
+ if (seen.has(key))
244
+ continue;
245
+ seen.add(key);
246
+ const stat = await fs.stat(filePath).catch(() => undefined);
247
+ if (!stat?.isFile())
248
+ continue;
249
+ const parsed = await fs
250
+ .readFile(filePath, "utf8")
251
+ .then((text) => parseJsonc(text))
252
+ .catch((error) => {
253
+ debug(`loadOpenCodePricingModels skipped ${filePath}: ${String(error)}`);
254
+ return undefined;
255
+ });
256
+ for (const model of extractOpenCodePricingModels(parsed)) {
257
+ const mergedKey = mergedModelMapKey(model);
258
+ const existing = merged.get(mergedKey);
259
+ merged.set(mergedKey, {
260
+ providerKey: model.providerKey ?? existing?.providerKey,
261
+ providerID: mergedProviderID(existing, model),
262
+ modelID: mergedModelID(existing, model),
263
+ modelKey: model.modelKey ?? existing?.modelKey,
264
+ cost: mergeCost(existing?.cost, model.cost),
265
+ options: mergeRecord(existing?.options, model.options),
266
+ headers: mergeRecord(existing?.headers, model.headers),
267
+ api: mergeRecord(existing?.api, model.api),
268
+ limit: mergeRecord(existing?.limit, model.limit),
269
+ });
270
+ }
271
+ }
272
+ return [...merged.values()];
273
+ }
package/dist/storage.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { dateKeyFromTimestamp, normalizeTimestampMs } from './storage_dates.js';
2
- import { authFilePath, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath } from './storage_paths.js';
3
- import type { CachedSessionUsage, IncrementalCursor, QuotaSidebarConfig, QuotaSidebarState, SessionState } from './types.js';
1
+ import { dateKeyFromTimestamp, normalizeTimestampMs } from "./storage_dates.js";
2
+ import { authFilePath, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath } from "./storage_paths.js";
3
+ import type { CachedSessionUsage, IncrementalCursor, QuotaSidebarConfig, QuotaSidebarState, SessionState } from "./types.js";
4
4
  export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, };
5
5
  export declare function quotaConfigPaths(worktree: string, directory: string): string[];
6
6
  export declare const defaultConfig: QuotaSidebarConfig;
package/dist/storage.js CHANGED
@@ -1,20 +1,20 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { asBoolean, asNumber, debug, isRecord, mapConcurrent, swallow, } from './helpers.js';
4
- import { discoverChunks, readDayChunk, safeWriteFile, writeDayChunk, } from './storage_chunks.js';
5
- import { dateKeyFromTimestamp, dateKeysInRange, dateStartFromKey, isDateKey, normalizeTimestampMs, } from './storage_dates.js';
6
- import { parseQuotaCache } from './storage_parse.js';
7
- import { authFilePath, chunkRootPathFromStateFile, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, } from './storage_paths.js';
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { asBoolean, asNumber, debug, isRecord, mapConcurrent, swallow, } from "./helpers.js";
4
+ import { discoverChunks, readDayChunk, safeWriteFile, writeDayChunk, } from "./storage_chunks.js";
5
+ import { dateKeyFromTimestamp, dateKeysInRange, dateStartFromKey, isDateKey, normalizeTimestampMs, } from "./storage_dates.js";
6
+ import { parseQuotaCache } from "./storage_parse.js";
7
+ import { authFilePath, chunkRootPathFromStateFile, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, } from "./storage_paths.js";
8
8
  export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeConfigDir, resolveOpencodeDataDir, stateFilePath, };
9
9
  export function quotaConfigPaths(worktree, directory) {
10
10
  const configDir = resolveOpencodeConfigDir();
11
11
  const configOverride = process.env.OPENCODE_QUOTA_CONFIG?.trim();
12
12
  return [
13
- path.join(configDir, 'quota-sidebar.config.json'),
14
- path.join(worktree, 'quota-sidebar.config.json'),
15
- path.join(directory, 'quota-sidebar.config.json'),
16
- path.join(worktree, '.opencode', 'quota-sidebar.config.json'),
17
- path.join(directory, '.opencode', 'quota-sidebar.config.json'),
13
+ path.join(configDir, "quota-sidebar.config.json"),
14
+ path.join(worktree, "quota-sidebar.config.json"),
15
+ path.join(directory, "quota-sidebar.config.json"),
16
+ path.join(worktree, ".opencode", "quota-sidebar.config.json"),
17
+ path.join(directory, ".opencode", "quota-sidebar.config.json"),
18
18
  ...(configOverride ? [path.resolve(configOverride)] : []),
19
19
  ];
20
20
  }
@@ -23,8 +23,7 @@ export const defaultConfig = {
23
23
  sidebar: {
24
24
  enabled: true,
25
25
  width: 36,
26
- titleMode: 'auto',
27
- multilineTitle: true,
26
+ titleMode: "auto",
28
27
  showCost: true,
29
28
  showQuota: true,
30
29
  wrapQuotaLines: true,
@@ -101,12 +100,11 @@ export async function loadConfig(paths) {
101
100
  sidebar: {
102
101
  enabled: asBoolean(sidebar.enabled, base.sidebar.enabled),
103
102
  width: Math.max(20, Math.min(60, asNumber(sidebar.width, base.sidebar.width))),
104
- titleMode: sidebar.titleMode === 'compact' ||
105
- sidebar.titleMode === 'multiline' ||
106
- sidebar.titleMode === 'auto'
103
+ titleMode: sidebar.titleMode === "compact" ||
104
+ sidebar.titleMode === "multiline" ||
105
+ sidebar.titleMode === "auto"
107
106
  ? sidebar.titleMode
108
- : (base.sidebar.titleMode ?? 'auto'),
109
- multilineTitle: asBoolean(sidebar.multilineTitle, base.sidebar.multilineTitle ?? true),
107
+ : (base.sidebar.titleMode ?? "auto"),
110
108
  showCost: asBoolean(sidebar.showCost, base.sidebar.showCost),
111
109
  showQuota: asBoolean(sidebar.showQuota, base.sidebar.showQuota),
112
110
  wrapQuotaLines: asBoolean(sidebar.wrapQuotaLines, base.sidebar.wrapQuotaLines),
@@ -142,17 +140,17 @@ export async function loadConfig(paths) {
142
140
  let config = defaultConfig;
143
141
  for (const originalPath of paths) {
144
142
  const filePath = path.resolve(originalPath);
145
- const key = process.platform === 'win32' ? filePath.toLowerCase() : filePath;
143
+ const key = process.platform === "win32" ? filePath.toLowerCase() : filePath;
146
144
  if (seen.has(key))
147
145
  continue;
148
146
  seen.add(key);
149
- const stat = await fs.stat(filePath).catch(swallow('loadConfig:stat'));
147
+ const stat = await fs.stat(filePath).catch(swallow("loadConfig:stat"));
150
148
  if (!stat || !stat.isFile())
151
149
  continue;
152
150
  const parsed = await fs
153
- .readFile(filePath, 'utf8')
151
+ .readFile(filePath, "utf8")
154
152
  .then((value) => JSON.parse(value))
155
- .catch(swallow('loadConfig:read'));
153
+ .catch(swallow("loadConfig:read"));
156
154
  if (!isRecord(parsed))
157
155
  continue;
158
156
  config = mergeLayer(config, parsed);
@@ -165,12 +163,12 @@ async function loadVersion2State(raw, statePath) {
165
163
  const titleEnabled = asBoolean(raw.titleEnabled, true);
166
164
  const quotaCache = parseQuotaCache(raw.quotaCache);
167
165
  const rootPath = chunkRootPathFromStateFile(statePath);
168
- const hasSessionDateMap = Object.prototype.hasOwnProperty.call(raw, 'sessionDateMap');
166
+ const hasSessionDateMap = Object.prototype.hasOwnProperty.call(raw, "sessionDateMap");
169
167
  const sessionDateMapRaw = isRecord(raw.sessionDateMap)
170
168
  ? raw.sessionDateMap
171
169
  : {};
172
170
  const sessionDateMap = Object.entries(sessionDateMapRaw).reduce((acc, [sessionID, value]) => {
173
- if (typeof value !== 'string')
171
+ if (typeof value !== "string")
174
172
  return acc;
175
173
  if (!isDateKey(value))
176
174
  return acc;
@@ -181,7 +179,7 @@ async function loadVersion2State(raw, statePath) {
181
179
  ? raw.deletedSessionDateMap
182
180
  : {};
183
181
  const deletedSessionDateMap = Object.entries(deletedSessionDateMapRaw).reduce((acc, [sessionID, value]) => {
184
- if (typeof value !== 'string')
182
+ if (typeof value !== "string")
185
183
  return acc;
186
184
  if (!isDateKey(value))
187
185
  return acc;
@@ -228,9 +226,9 @@ async function loadVersion2State(raw, statePath) {
228
226
  }
229
227
  export async function loadState(statePath) {
230
228
  const raw = await fs
231
- .readFile(statePath, 'utf8')
229
+ .readFile(statePath, "utf8")
232
230
  .then((value) => JSON.parse(value))
233
- .catch(swallow('loadState'));
231
+ .catch(swallow("loadState"));
234
232
  if (!isRecord(raw))
235
233
  return defaultState();
236
234
  if (raw.version === 2)
@@ -495,6 +493,7 @@ export async function updateSessionsInDayChunks(statePath, updates) {
495
493
  ...existing,
496
494
  usage: item.usage,
497
495
  cursor: item.cursor,
496
+ dirty: false,
498
497
  };
499
498
  changed = true;
500
499
  }
@@ -1,3 +1,3 @@
1
- import type { QuotaSnapshot, SessionState } from './types.js';
1
+ import type { QuotaSnapshot, SessionState } from "./types.js";
2
2
  export declare function parseSessionState(value: unknown): SessionState | undefined;
3
3
  export declare function parseQuotaCache(value: unknown): Record<string, QuotaSnapshot>;
@@ -1,13 +1,13 @@
1
- import { asNumber, isRecord } from './helpers.js';
2
- import { isSupportedQuotaSnapshot } from './supported_quota.js';
3
- import { normalizeTimestampMs } from './storage_dates.js';
1
+ import { asNumber, isRecord } from "./helpers.js";
2
+ import { isSupportedQuotaSnapshot } from "./supported_quota.js";
3
+ import { normalizeTimestampMs } from "./storage_dates.js";
4
4
  function parseSessionTitleState(value) {
5
5
  if (!isRecord(value))
6
6
  return undefined;
7
- if (typeof value.baseTitle !== 'string')
7
+ if (typeof value.baseTitle !== "string")
8
8
  return undefined;
9
9
  if (value.lastAppliedTitle !== undefined &&
10
- typeof value.lastAppliedTitle !== 'string') {
10
+ typeof value.lastAppliedTitle !== "string") {
11
11
  return undefined;
12
12
  }
13
13
  return {
@@ -69,13 +69,13 @@ function parseRecentProviders(value) {
69
69
  const parsed = value
70
70
  .filter((item) => isRecord(item))
71
71
  .map((item) => ({
72
- providerID: typeof item.providerID === 'string' && item.providerID
72
+ providerID: typeof item.providerID === "string" && item.providerID
73
73
  ? item.providerID
74
74
  : undefined,
75
75
  completedAt: asNumber(item.completedAt),
76
76
  }))
77
- .filter((item) => typeof item.providerID === 'string' &&
78
- typeof item.completedAt === 'number' &&
77
+ .filter((item) => typeof item.providerID === "string" &&
78
+ typeof item.completedAt === "number" &&
79
79
  Number.isFinite(item.completedAt));
80
80
  return parsed.length > 0 ? parsed : undefined;
81
81
  }
@@ -92,6 +92,12 @@ function parseCachedUsage(value) {
92
92
  }, {});
93
93
  return {
94
94
  billingVersion: asNumber(value.billingVersion),
95
+ pricingFingerprint: typeof value.pricingFingerprint === "string"
96
+ ? value.pricingFingerprint
97
+ : undefined,
98
+ pricingKeys: Array.isArray(value.pricingKeys)
99
+ ? value.pricingKeys.filter((item) => typeof item === "string" && !!item)
100
+ : undefined,
95
101
  input: asNumber(value.input, 0),
96
102
  output: asNumber(value.output, 0),
97
103
  reasoning: asNumber(value.reasoning, 0),
@@ -113,81 +119,81 @@ function parseQuotaSnapshot(value) {
113
119
  if (!checkedAt)
114
120
  return undefined;
115
121
  const status = value.status;
116
- if (status !== 'ok' &&
117
- status !== 'unavailable' &&
118
- status !== 'unsupported' &&
119
- status !== 'error') {
122
+ if (status !== "ok" &&
123
+ status !== "unavailable" &&
124
+ status !== "unsupported" &&
125
+ status !== "error") {
120
126
  return undefined;
121
127
  }
122
- const label = typeof value.label === 'string' ? value.label : '';
123
- const adapterID = typeof value.adapterID === 'string' ? value.adapterID : undefined;
124
- const shortLabel = typeof value.shortLabel === 'string' ? value.shortLabel : undefined;
125
- const sortOrder = typeof value.sortOrder === 'number' ? value.sortOrder : undefined;
128
+ const label = typeof value.label === "string" ? value.label : "";
129
+ const adapterID = typeof value.adapterID === "string" ? value.adapterID : undefined;
130
+ const shortLabel = typeof value.shortLabel === "string" ? value.shortLabel : undefined;
131
+ const sortOrder = typeof value.sortOrder === "number" ? value.sortOrder : undefined;
126
132
  const balance = isRecord(value.balance)
127
133
  ? {
128
- amount: typeof value.balance.amount === 'number' ? value.balance.amount : 0,
129
- currency: typeof value.balance.currency === 'string'
134
+ amount: typeof value.balance.amount === "number" ? value.balance.amount : 0,
135
+ currency: typeof value.balance.currency === "string"
130
136
  ? value.balance.currency
131
- : '$',
137
+ : "$",
132
138
  }
133
139
  : undefined;
134
140
  const windows = Array.isArray(value.windows)
135
141
  ? value.windows
136
142
  .filter((window) => isRecord(window))
137
143
  .map((window) => ({
138
- label: typeof window.label === 'string' ? window.label : '',
139
- showPercent: typeof window.showPercent === 'boolean'
144
+ label: typeof window.label === "string" ? window.label : "",
145
+ showPercent: typeof window.showPercent === "boolean"
140
146
  ? window.showPercent
141
147
  : undefined,
142
- resetLabel: typeof window.resetLabel === 'string'
148
+ resetLabel: typeof window.resetLabel === "string"
143
149
  ? window.resetLabel
144
150
  : undefined,
145
- note: typeof window.note === 'string' ? window.note : undefined,
146
- remainingPercent: typeof window.remainingPercent === 'number'
151
+ note: typeof window.note === "string" ? window.note : undefined,
152
+ remainingPercent: typeof window.remainingPercent === "number"
147
153
  ? window.remainingPercent
148
154
  : undefined,
149
- usedPercent: typeof window.usedPercent === 'number'
155
+ usedPercent: typeof window.usedPercent === "number"
150
156
  ? window.usedPercent
151
157
  : undefined,
152
- resetAt: typeof window.resetAt === 'string' ? window.resetAt : undefined,
158
+ resetAt: typeof window.resetAt === "string" ? window.resetAt : undefined,
153
159
  }))
154
160
  .filter((window) => window.label || window.remainingPercent !== undefined)
155
161
  : undefined;
156
162
  const stale = isRecord(value.stale)
157
163
  ? (() => {
158
- const staleReasonKind = value.stale.staleReasonKind === 'timeout' ||
159
- value.stale.staleReasonKind === 'network' ||
160
- value.stale.staleReasonKind === 'http_5xx' ||
161
- value.stale.staleReasonKind === 'http_transient' ||
162
- value.stale.staleReasonKind === 'invalid_response' ||
163
- value.stale.staleReasonKind === 'unknown'
164
+ const staleReasonKind = value.stale.staleReasonKind === "timeout" ||
165
+ value.stale.staleReasonKind === "network" ||
166
+ value.stale.staleReasonKind === "http_5xx" ||
167
+ value.stale.staleReasonKind === "http_transient" ||
168
+ value.stale.staleReasonKind === "invalid_response" ||
169
+ value.stale.staleReasonKind === "unknown"
164
170
  ? value.stale.staleReasonKind
165
- : 'unknown';
171
+ : "unknown";
166
172
  return {
167
- staleAt: typeof value.stale.staleAt === 'number' ? value.stale.staleAt : 0,
168
- staleReason: typeof value.stale.staleReason === 'string'
173
+ staleAt: typeof value.stale.staleAt === "number" ? value.stale.staleAt : 0,
174
+ staleReason: typeof value.stale.staleReason === "string"
169
175
  ? value.stale.staleReason
170
- : '',
176
+ : "",
171
177
  staleReasonKind,
172
178
  };
173
179
  })()
174
180
  : undefined;
175
181
  const parsed = {
176
- providerID: typeof value.providerID === 'string' ? value.providerID : label,
182
+ providerID: typeof value.providerID === "string" ? value.providerID : label,
177
183
  adapterID,
178
184
  label,
179
185
  shortLabel,
180
186
  sortOrder,
181
187
  status,
182
188
  checkedAt,
183
- remainingPercent: typeof value.remainingPercent === 'number'
189
+ remainingPercent: typeof value.remainingPercent === "number"
184
190
  ? value.remainingPercent
185
191
  : undefined,
186
- usedPercent: typeof value.usedPercent === 'number' ? value.usedPercent : undefined,
187
- resetAt: typeof value.resetAt === 'string' ? value.resetAt : undefined,
188
- expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,
192
+ usedPercent: typeof value.usedPercent === "number" ? value.usedPercent : undefined,
193
+ resetAt: typeof value.resetAt === "string" ? value.resetAt : undefined,
194
+ expiresAt: typeof value.expiresAt === "string" ? value.expiresAt : undefined,
189
195
  balance,
190
- note: typeof value.note === 'string' ? value.note : undefined,
196
+ note: typeof value.note === "string" ? value.note : undefined,
191
197
  windows,
192
198
  stale: stale && stale.staleAt > 0 && stale.staleReason ? stale : undefined,
193
199
  };
@@ -223,10 +229,10 @@ function parseCursor(value) {
223
229
  return undefined;
224
230
  const idsRaw = value.lastMessageIdsAtTime;
225
231
  const lastMessageIdsAtTime = Array.isArray(idsRaw)
226
- ? idsRaw.filter((item) => typeof item === 'string' && !!item)
232
+ ? idsRaw.filter((item) => typeof item === "string" && !!item)
227
233
  : undefined;
228
234
  return {
229
- lastMessageId: typeof value.lastMessageId === 'string' ? value.lastMessageId : undefined,
235
+ lastMessageId: typeof value.lastMessageId === "string" ? value.lastMessageId : undefined,
230
236
  lastMessageTime: asNumber(value.lastMessageTime),
231
237
  lastMessageIdsAtTime: lastMessageIdsAtTime && lastMessageIdsAtTime.length
232
238
  ? Array.from(new Set(lastMessageIdsAtTime)).sort()
@@ -245,7 +251,7 @@ export function parseSessionState(value) {
245
251
  return {
246
252
  ...title,
247
253
  createdAt,
248
- parentID: typeof value.parentID === 'string' ? value.parentID : undefined,
254
+ parentID: typeof value.parentID === "string" ? value.parentID : undefined,
249
255
  expiryToastShown: value.expiryToastShown === true,
250
256
  usage: parseCachedUsage(value.usage),
251
257
  dirty: value.dirty === true,
@@ -15,6 +15,7 @@ export declare function resolveOpencodeDataDir(): string;
15
15
  * Uses XDG config conventions with optional plugin-scoped override.
16
16
  */
17
17
  export declare function resolveOpencodeConfigDir(): string;
18
+ export declare function opencodeConfigPaths(worktree: string, directory: string): string[];
18
19
  export declare function stateFilePath(dataDir: string): string;
19
20
  export declare function authFilePath(dataDir: string): string;
20
21
  export declare function chunkRootPathFromStateFile(statePath: string): string;