@camerontaylor/paseo-server 0.11.0-beta.1.fork.1 → 0.11.0-beta.3.fork.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.
Files changed (55) hide show
  1. package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -3
  2. package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +131 -0
  3. package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +123 -70
  4. package/dist/server/builtin-plugins/claude-usage-source/shared/input.ts +13 -1
  5. package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +2 -3
  6. package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +131 -0
  7. package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +103 -33
  8. package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +12 -1
  9. package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
  10. package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +59 -34
  11. package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +7 -1
  12. package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
  13. package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
  14. package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
  15. package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
  16. package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
  17. package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
  18. package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
  19. package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
  20. package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
  21. package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
  22. package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
  23. package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
  24. package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
  25. package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
  26. package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
  27. package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
  28. package/dist/server/builtin-plugins/package.json +1 -1
  29. package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
  30. package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
  31. package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
  32. package/dist/server/server/orchestration-skills/internal/operations.js +19 -5
  33. package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
  34. package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
  35. package/dist/server/server/plugins/builtin/index.js +6 -0
  36. package/dist/server/server/plugins/compiler.js +4 -0
  37. package/dist/server/server/plugins/index.d.ts +18 -3
  38. package/dist/server/server/plugins/index.js +1 -2
  39. package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
  40. package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
  41. package/dist/server/server/plugins/plugin-process.js +2 -7
  42. package/dist/server/server/plugins/runtime.d.ts +0 -1
  43. package/dist/server/server/plugins/runtime.js +0 -11
  44. package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
  45. package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
  46. package/dist/server/server/plugins/usage-sources/index.d.ts +7 -9
  47. package/dist/server/server/plugins/usage-sources/index.js +115 -71
  48. package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js → index-980574f58cb292611c985646d625a892.js} +12 -12
  49. package/dist/server/web-ui/_expo/static/js/web/index-980574f58cb292611c985646d625a892.js.br +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js.gz → index-980574f58cb292611c985646d625a892.js.gz} +0 -0
  51. package/dist/server/web-ui/index.html +1 -1
  52. package/dist/server/web-ui/index.html.br +0 -0
  53. package/dist/server/web-ui/index.html.gz +0 -0
  54. package/package.json +6 -6
  55. package/dist/server/web-ui/_expo/static/js/web/index-5887a06cac5928321e7fd84d0fdf68a4.js.br +0 -0
@@ -10,12 +10,24 @@ export interface SkillSyncResult {
10
10
  changedFiles: number;
11
11
  processedSkills: number;
12
12
  }
13
+ interface ManagedFilesManifest {
14
+ version: 1;
15
+ files: Record<string, string>;
16
+ }
13
17
  export declare function listFilesRecursive(rootDir: string): Promise<string[]>;
18
+ export declare function readManagedFilesManifest(dstDir: string): Promise<ManagedFilesManifest | null>;
14
19
  export interface RemoveSkillTargets {
15
20
  agentsDir: string;
16
21
  claudeDir: string;
17
22
  codexDir: string;
18
23
  }
19
24
  export declare function removeSkill(skillName: string, targets: RemoveSkillTargets): Promise<void>;
25
+ /**
26
+ * Codex also discovers .agents/skills, so its dedicated copy is a leftover. When
27
+ * the Codex skill directory resolves to the shared or Claude copy, it is that
28
+ * copy, and retiring it would delete the install.
29
+ */
30
+ export declare function isLegacyCodexCopy(skillName: string, targets: RemoveSkillTargets): Promise<boolean>;
20
31
  export declare function syncSkills(options: SkillSyncOptions): Promise<SkillSyncResult>;
32
+ export {};
21
33
  //# sourceMappingURL=sync.d.ts.map
@@ -31,7 +31,7 @@ export async function listFilesRecursive(rootDir) {
31
31
  await walk(rootDir);
32
32
  return out;
33
33
  }
34
- async function readManagedFilesManifest(dstDir) {
34
+ export async function readManagedFilesManifest(dstDir) {
35
35
  const raw = await fs
36
36
  .readFile(path.join(dstDir, MANAGED_FILES_MANIFEST), "utf-8")
37
37
  .catch(() => null);
@@ -152,6 +152,49 @@ export async function removeSkill(skillName, targets) {
152
152
  await fs.rm(p, { recursive: true, force: true });
153
153
  }
154
154
  }
155
+ async function isSameDirectory(a, b) {
156
+ const [realA, realB] = await Promise.all([a, b].map((p) => fs.realpath(p).catch(() => null)));
157
+ return realA !== null && realA === realB;
158
+ }
159
+ /**
160
+ * Codex also discovers .agents/skills, so its dedicated copy is a leftover. When
161
+ * the Codex skill directory resolves to the shared or Claude copy, it is that
162
+ * copy, and retiring it would delete the install.
163
+ */
164
+ export async function isLegacyCodexCopy(skillName, targets) {
165
+ const codexSkillDir = path.join(targets.codexDir, skillName);
166
+ for (const root of [targets.agentsDir, targets.claudeDir]) {
167
+ if (await isSameDirectory(codexSkillDir, path.join(root, skillName)))
168
+ return false;
169
+ }
170
+ return true;
171
+ }
172
+ async function retireManagedSkill(dstDir) {
173
+ const info = await fs.lstat(dstDir).catch(() => null);
174
+ if (!info?.isDirectory())
175
+ return 0;
176
+ const manifest = await readManagedFilesManifest(dstDir);
177
+ if (!manifest)
178
+ return 0;
179
+ const files = Object.keys(manifest.files);
180
+ await assertManagedPathsStayInsideSkill(dstDir, [MANAGED_FILES_MANIFEST, ...files]);
181
+ const removed = [];
182
+ for (const [rel, previousHash] of Object.entries(manifest.files)) {
183
+ const filePath = path.join(dstDir, rel);
184
+ const currentHash = await hashFile(filePath).catch(() => null);
185
+ if (currentHash !== previousHash)
186
+ continue;
187
+ await fs.rm(filePath);
188
+ removed.push(rel);
189
+ }
190
+ await pruneEmptyParentDirs(dstDir, removed);
191
+ await fs.rm(path.join(dstDir, MANAGED_FILES_MANIFEST));
192
+ await fs.rmdir(dstDir).catch((error) => {
193
+ if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT")
194
+ throw error;
195
+ });
196
+ return removed.length + 1;
197
+ }
155
198
  export async function syncSkills(options) {
156
199
  let changedFiles = 0;
157
200
  let processedSkills = 0;
@@ -163,7 +206,10 @@ export async function syncSkills(options) {
163
206
  try {
164
207
  changedFiles += await syncDirectoryFiles(bundleSkillDir, path.join(options.agentsDir, skillName));
165
208
  changedFiles += await syncDirectoryFiles(bundleSkillDir, path.join(options.claudeDir, skillName));
166
- changedFiles += await syncDirectoryFiles(bundleSkillDir, path.join(options.codexDir, skillName));
209
+ // Retain user edits to the old dedicated Codex copy.
210
+ if (await isLegacyCodexCopy(skillName, options)) {
211
+ changedFiles += await retireManagedSkill(path.join(options.codexDir, skillName));
212
+ }
167
213
  processedSkills++;
168
214
  }
169
215
  catch (error) {
@@ -16,6 +16,12 @@ export const builtinPlugins = [
16
16
  ];
17
17
  export function resolveBuiltinPluginsRoot(moduleUrl = import.meta.url) {
18
18
  const moduleDir = path.dirname(fileURLToPath(moduleUrl));
19
+ const archiveSegment = `${path.sep}app.asar${path.sep}`;
20
+ const archiveIndex = moduleDir.indexOf(archiveSegment);
21
+ if (archiveIndex !== -1) {
22
+ // esbuild runs outside Electron's filesystem shim and cannot read archive entries.
23
+ return path.join(moduleDir.slice(0, archiveIndex), "builtin-plugins");
24
+ }
19
25
  const candidates = [
20
26
  path.resolve(moduleDir, "..", "..", "..", "builtin-plugins"),
21
27
  path.resolve(moduleDir, "..", "..", "..", "..", "..", "..", "plugins"),
@@ -320,6 +320,10 @@ async function compileTarget(entryPath, target) {
320
320
  jsx: "automatic",
321
321
  platform: target === "server" ? "node" : "neutral",
322
322
  target: target === "server" ? "node20" : "es2020",
323
+ // The neutral platform reads no package.json entry fields, so packages without
324
+ // `exports` would not resolve. Read the same fields as esbuild's browser platform
325
+ // minus `browser`, since the bundle also runs in React Native.
326
+ mainFields: target === "client" ? ["module", "main"] : undefined,
323
327
  // Metro lowers async syntax before Hermes sees app code. Plugin client bundles bypass Metro,
324
328
  // so apply the same compatibility transform before the app evaluates them from source.
325
329
  supported: target === "client" ? { "async-await": false } : undefined,
@@ -17,7 +17,6 @@ interface PluginRuntimePort {
17
17
  clearLogs(pluginId: string): void;
18
18
  getProviderRegistrations?(pluginId: string): readonly PluginProviderMetadata[];
19
19
  getUsageSourceRegistrations(pluginId: string): readonly PluginUsageSourceMetadata[];
20
- identifyUsage: PluginRuntime["identifyUsage"];
21
20
  fetchUsage: PluginRuntime["fetchUsage"];
22
21
  discoverUsage: PluginRuntime["discoverUsage"];
23
22
  connectProvider: PluginRuntime["connectProvider"];
@@ -76,7 +75,7 @@ export declare class PluginService {
76
75
  sourceId: string;
77
76
  sourceLabel: string;
78
77
  report: {
79
- status: "error" | "unavailable" | "available";
78
+ status: "available";
80
79
  windows: {
81
80
  id: string;
82
81
  label: string;
@@ -106,7 +105,23 @@ export declare class PluginService {
106
105
  value: string;
107
106
  tone?: "default" | "warning" | "danger" | "ok" | undefined;
108
107
  }[] | undefined;
109
- error?: string | undefined;
108
+ } | {
109
+ status: "unavailable";
110
+ problem: {
111
+ kind: "expired";
112
+ expiresAt: string;
113
+ refreshedBy?: string | undefined;
114
+ } | {
115
+ kind: "rejected";
116
+ status: number;
117
+ refreshedBy?: string | undefined;
118
+ } | {
119
+ kind: "no_quota";
120
+ detail: string;
121
+ };
122
+ } | {
123
+ status: "error";
124
+ error: string;
110
125
  };
111
126
  icon?: string | undefined;
112
127
  }[]>;
@@ -25,7 +25,6 @@ export class PluginService {
25
25
  this.errors = new Map();
26
26
  this.listeners = new Set();
27
27
  this.providers = new Map();
28
- this.usageSources = new UsageSourceRegistry();
29
28
  this.usageSourceIdsByPlugin = new Map();
30
29
  this.providerIdsByPlugin = new Map();
31
30
  this.providerListeners = new Set();
@@ -43,6 +42,7 @@ export class PluginService {
43
42
  return request;
44
43
  };
45
44
  this.logger = logger.child({ module: "plugin-service" });
45
+ this.usageSources = new UsageSourceRegistry(Date.now, 300000, this.logger);
46
46
  this.runtime =
47
47
  dependencies.runtime ??
48
48
  new PluginRuntime(logger, daemonVersion, {
@@ -484,7 +484,6 @@ export class PluginService {
484
484
  ]
485
485
  : result;
486
486
  },
487
- identify: (input) => this.runtime.identifyUsage(pluginId, source.id, input),
488
487
  fetch: (input) => {
489
488
  return this.runtime.fetchUsage(pluginId, source.id, input);
490
489
  },
@@ -40,11 +40,6 @@ export type PluginProcessRequest = {
40
40
  } | {
41
41
  type: "hook.cancel";
42
42
  requestId: string;
43
- } | {
44
- type: "usage.identify";
45
- requestId: string;
46
- sourceId: string;
47
- input: unknown;
48
43
  } | {
49
44
  type: "usage.fetch";
50
45
  requestId: string;
@@ -83,14 +83,6 @@ export const PluginProcessRequestSchema = z.discriminatedUnion("type", [
83
83
  })
84
84
  .strict(),
85
85
  z.object({ type: z.literal("hook.cancel"), requestId: z.string() }).strict(),
86
- z
87
- .object({
88
- type: z.literal("usage.identify"),
89
- requestId: z.string(),
90
- sourceId: z.string(),
91
- input: z.unknown(),
92
- })
93
- .strict(),
94
86
  z
95
87
  .object({
96
88
  type: z.literal("usage.fetch"),
@@ -296,15 +296,12 @@ export function createPluginWorker(options) {
296
296
  if (message.type === "usage.discover")
297
297
  return jsonTransportValue(await source.discover());
298
298
  const input = await source.input.parseAsync(message.input);
299
- return jsonTransportValue(message.type === "usage.identify"
300
- ? await source.identify(input)
301
- : await source.fetch(input));
299
+ return jsonTransportValue(await source.fetch(input));
302
300
  })().then((output) => send({ type: "result", requestId: message.requestId, output }), (error) => send({ type: "error", requestId: message.requestId, error: describeError(error) }));
303
301
  }
304
302
  function rejectWhileStopping(message) {
305
303
  if (message.type === "provider.status" ||
306
304
  message.type === "provider.catalog_key" ||
307
- message.type === "usage.identify" ||
308
305
  message.type === "usage.fetch" ||
309
306
  message.type === "usage.discover") {
310
307
  send({ type: "error", requestId: message.requestId, error: "Plugin is stopping" });
@@ -389,9 +386,7 @@ export function createPluginWorker(options) {
389
386
  })().catch((error) => send({ type: "error", requestId: message.requestId, error: describeError(error) }));
390
387
  return;
391
388
  }
392
- if (message.type === "usage.identify" ||
393
- message.type === "usage.fetch" ||
394
- message.type === "usage.discover") {
389
+ if (message.type === "usage.fetch" || message.type === "usage.discover") {
395
390
  handleUsageRequest(message);
396
391
  return;
397
392
  }
@@ -57,7 +57,6 @@ export declare class PluginRuntime {
57
57
  }>;
58
58
  getProviderRegistrations(pluginId: string): readonly PluginProviderMetadata[];
59
59
  getUsageSourceRegistrations(pluginId: string): readonly PluginUsageSourceMetadata[];
60
- identifyUsage(pluginId: string, sourceId: string, input: unknown): Promise<unknown>;
61
60
  fetchUsage(pluginId: string, sourceId: string, input: unknown): Promise<unknown>;
62
61
  discoverUsage(pluginId: string, sourceId: string): Promise<unknown>;
63
62
  getProviderStatus(pluginId: string, providerId: string, request: ProviderStatusRequest): Promise<ProviderStatus>;
@@ -241,17 +241,6 @@ export class PluginRuntime {
241
241
  getUsageSourceRegistrations(pluginId) {
242
242
  return this.plugins.get(pluginId)?.usageSources ?? [];
243
243
  }
244
- identifyUsage(pluginId, sourceId, input) {
245
- const loaded = this.plugins.get(pluginId);
246
- if (!loaded)
247
- throw new Error(`Plugin is not available: ${pluginId}`);
248
- return this.request(loaded, {
249
- type: "usage.identify",
250
- requestId: randomUUID(),
251
- sourceId,
252
- input,
253
- });
254
- }
255
244
  fetchUsage(pluginId, sourceId, input) {
256
245
  const loaded = this.plugins.get(pluginId);
257
246
  if (!loaded)
@@ -11,12 +11,21 @@ export default function contribute(server) {
11
11
  { account: "one" },
12
12
  { account: "bad", extra: true },
13
13
  { account: "throws" },
14
+ { account: "expired" },
14
15
  ];
15
- return inputs;
16
+ return inputs.map((input) => ({ key: String(input.account), input }));
16
17
  },
17
- identify: async (input) => ({ key: input.account }),
18
18
  fetch: async (input) => {
19
19
  const account = input.account;
20
+ if (account === "expired")
21
+ return {
22
+ status: "unavailable",
23
+ problem: {
24
+ kind: "expired",
25
+ expiresAt: new Date(Date.now() - 3600000).toISOString(),
26
+ refreshedBy: "claude",
27
+ },
28
+ };
20
29
  if (account === "throws")
21
30
  throw new Error("fixture failure");
22
31
  fetches++;
@@ -11,12 +11,21 @@ export default function contribute(server) {
11
11
  { account: "one" },
12
12
  { account: "bad", extra: true },
13
13
  { account: "throws" },
14
+ { account: "expired" },
14
15
  ];
15
- return inputs;
16
+ return inputs.map((input) => ({ key: String(input.account), input }));
16
17
  },
17
- identify: async (input) => ({ key: input.account }),
18
18
  fetch: async (input) => {
19
19
  const account = input.account;
20
+ if (account === "expired")
21
+ return {
22
+ status: "unavailable",
23
+ problem: {
24
+ kind: "expired",
25
+ expiresAt: new Date(Date.now() - 3600000).toISOString(),
26
+ refreshedBy: "claude",
27
+ },
28
+ };
20
29
  if (account === "throws")
21
30
  throw new Error("fixture failure");
22
31
  fetches++;
@@ -1,29 +1,26 @@
1
+ import type { Logger } from "pino";
1
2
  import { type ProviderUsage, type UsageReportEntry } from "@camerontaylor/paseo-protocol/messages";
2
3
  export interface UsageSource {
3
4
  id: string;
4
5
  label: string;
5
6
  icon?: string;
6
- discover(): Promise<unknown[]>;
7
- identify(input: unknown): Promise<{
8
- key: string;
9
- label?: string;
10
- } | null>;
7
+ discover(): Promise<unknown>;
11
8
  fetch(input: unknown): Promise<unknown>;
12
9
  }
13
- /** Owns account identity, the latest input for each report, and the five-minute fetch cache. */
10
+ /** Owns account identity, ordered logins for each account, and the five-minute fetch cache. */
14
11
  export declare class UsageSourceRegistry {
15
12
  private readonly now;
16
13
  private readonly ttlMs;
14
+ private readonly logger;
17
15
  private generation;
18
16
  private readonly sources;
19
17
  private readonly known;
20
18
  private readonly cache;
21
19
  private readonly pending;
22
- constructor(now?: () => number, ttlMs?: number);
20
+ constructor(now?: () => number, ttlMs?: number, logger?: Pick<Logger, "warn">);
23
21
  register(source: UsageSource): void;
24
22
  unregister(id: string): void;
25
23
  invalidateReports(predicate: (id: string) => boolean): void;
26
- private identify;
27
24
  listReports(options?: {
28
25
  forceRefresh?: boolean;
29
26
  reportIds?: string[];
@@ -35,6 +32,7 @@ export declare class UsageSourceRegistry {
35
32
  }>;
36
33
  private fetchId;
37
34
  private writeCache;
38
- private errorEntry;
35
+ private fetchWithFallback;
36
+ private fetchLogin;
39
37
  }
40
38
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,11 @@
1
+ import { z } from "zod";
1
2
  import { UsageReportSchema, } from "@camerontaylor/paseo-protocol/messages";
2
- /** Owns account identity, the latest input for each report, and the five-minute fetch cache. */
3
+ /** Owns account identity, ordered logins for each account, and the five-minute fetch cache. */
3
4
  export class UsageSourceRegistry {
4
- constructor(now = Date.now, ttlMs = 300000) {
5
+ constructor(now = Date.now, ttlMs = 300000, logger = { warn: console.warn }) {
5
6
  this.now = now;
6
7
  this.ttlMs = ttlMs;
8
+ this.logger = logger;
7
9
  this.generation = 0;
8
10
  this.sources = new Map();
9
11
  this.known = new Map();
@@ -27,26 +29,6 @@ export class UsageSourceRegistry {
27
29
  map.delete(id);
28
30
  }
29
31
  }
30
- async identify(source, input) {
31
- try {
32
- const account = await source.identify(input);
33
- if (account === null)
34
- return null;
35
- if (typeof account !== "object" ||
36
- (account.label !== undefined && typeof account.label !== "string"))
37
- throw new Error(`Invalid account identity from ${source.id}`);
38
- if (typeof account.key !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(account.key))
39
- throw new Error(`Invalid account key from ${source.id}`);
40
- const id = `${source.id}:${account.key}`;
41
- this.known.set(id, { source, input, label: account.label });
42
- return id;
43
- }
44
- catch (error) {
45
- const id = `${source.id}:!error`;
46
- this.writeCache(id, this.errorEntry(source, id, error));
47
- return id;
48
- }
49
- }
50
32
  async listReports(options = {}) {
51
33
  const generation = this.generation;
52
34
  const discovered = !options.reportIds ||
@@ -56,26 +38,68 @@ export class UsageSourceRegistry {
56
38
  if (generation !== this.generation)
57
39
  return this.listReports(options);
58
40
  const ids = options.reportIds ?? discovered;
59
- const reports = await Promise.all([...new Set(ids)]
60
- .filter((id) => this.known.has(id) || this.cache.has(id))
61
- .map((id) => this.fetchId(id, options.forceRefresh)));
41
+ const reports = await Promise.all([...new Set(ids)].flatMap((id) => {
42
+ const known = this.known.get(id);
43
+ return known ? [this.fetchId(id, known, options.forceRefresh)] : [];
44
+ }));
62
45
  return generation === this.generation ? reports : this.listReports(options);
63
46
  }
64
47
  async discoverReportIds() {
48
+ const generation = this.generation;
65
49
  const discovered = await Promise.all([...this.sources.values()].map(async (source) => {
66
50
  try {
67
- const inputs = await source.discover();
68
- if (!Array.isArray(inputs))
69
- throw new Error("Usage discovery must return an array");
70
- return (await Promise.all(inputs.map((input) => this.identify(source, input)))).filter((id) => id !== null);
51
+ const accounts = z
52
+ .array(z.union([
53
+ z.object({
54
+ key: z.string().regex(/^[A-Za-z0-9._-]{1,128}$/),
55
+ label: z.string().optional(),
56
+ input: z.json(),
57
+ }),
58
+ z
59
+ .object({
60
+ providerId: z.string().regex(/^[A-Za-z0-9._-]{1,119}$/),
61
+ label: z.string(),
62
+ })
63
+ .catchall(z.json()),
64
+ ]))
65
+ .parse(await source.discover());
66
+ if (generation !== this.generation)
67
+ return [];
68
+ const knownReports = new Map();
69
+ const ids = [];
70
+ for (const account of accounts) {
71
+ const key = "providerId" in account ? `provider.${account.providerId}` : account.key;
72
+ const input = "providerId" in account ? account : account.input;
73
+ const id = `${source.id}:${key}`;
74
+ ids.push(id);
75
+ const known = knownReports.get(id);
76
+ if (known)
77
+ known.inputs.push(input);
78
+ else
79
+ knownReports.set(id, { source, inputs: [input], label: account.label });
80
+ }
81
+ for (const key of this.known.keys()) {
82
+ if (key.startsWith(`${source.id}:`))
83
+ this.known.delete(key);
84
+ }
85
+ for (const [id, known] of knownReports)
86
+ this.known.set(id, known);
87
+ return ids;
71
88
  }
72
89
  catch (error) {
73
- const id = `${source.id}:!error`;
74
- this.writeCache(id, this.errorEntry(source, id, error));
75
- return [id];
90
+ if (generation !== this.generation)
91
+ return [];
92
+ for (const key of this.known.keys()) {
93
+ if (key.startsWith(`${source.id}:`))
94
+ this.known.delete(key);
95
+ }
96
+ this.logger.warn({ sourceId: source.id, err: error }, "Usage source discovery failed");
97
+ return [];
76
98
  }
77
99
  }));
78
100
  const ids = discovered.flat();
101
+ if (generation !== this.generation)
102
+ return [];
79
103
  const current = new Set(ids);
80
104
  for (const id of this.known.keys())
81
105
  if (!current.has(id)) {
@@ -104,11 +128,11 @@ export class UsageSourceRegistry {
104
128
  providerId: custom ? entry.id.slice(prefix.length) : entry.sourceId,
105
129
  displayName,
106
130
  status: entry.report.status,
107
- planLabel: entry.report.planLabel ?? null,
108
- windows: entry.report.windows,
109
- balances: entry.report.balances ?? [],
110
- details: entry.report.details ?? [],
111
- error: entry.report.error ?? null,
131
+ planLabel: entry.report.status === "available" ? (entry.report.planLabel ?? null) : null,
132
+ windows: entry.report.status === "available" ? entry.report.windows : [],
133
+ balances: entry.report.status === "available" ? (entry.report.balances ?? []) : [],
134
+ details: entry.report.status === "available" ? (entry.report.details ?? []) : [],
135
+ error: legacyError(entry.report, this.now()),
112
136
  };
113
137
  if (custom)
114
138
  provider.baseProviderId = entry.sourceId;
@@ -116,11 +140,8 @@ export class UsageSourceRegistry {
116
140
  }),
117
141
  };
118
142
  }
119
- fetchId(id, forceRefresh = false) {
120
- const known = this.known.get(id);
143
+ fetchId(id, known, forceRefresh = false) {
121
144
  const cached = this.cache.get(id);
122
- if (!known)
123
- return Promise.resolve(cached.entry);
124
145
  if (!forceRefresh && cached && this.now() - cached.at < this.ttlMs)
125
146
  return Promise.resolve(cached.entry);
126
147
  const pending = this.pending.get(id);
@@ -128,22 +149,15 @@ export class UsageSourceRegistry {
128
149
  return pending;
129
150
  const generation = this.generation;
130
151
  const request = (async () => {
131
- let entry;
132
- try {
133
- const report = UsageReportSchema.parse(await known.source.fetch(known.input));
134
- entry = {
135
- id,
136
- sourceId: known.source.id,
137
- sourceLabel: known.source.label,
138
- icon: known.source.icon,
139
- account: { label: known.label },
140
- fetchedAt: new Date(this.now()).toISOString(),
141
- report,
142
- };
143
- }
144
- catch (error) {
145
- entry = this.errorEntry(known.source, id, error, known.label);
146
- }
152
+ const entry = {
153
+ id,
154
+ sourceId: known.source.id,
155
+ sourceLabel: known.source.label,
156
+ icon: known.source.icon,
157
+ account: { label: known.label },
158
+ fetchedAt: new Date(this.now()).toISOString(),
159
+ report: await this.fetchWithFallback(known),
160
+ };
147
161
  if (generation === this.generation && this.known.get(id) === known)
148
162
  this.writeCache(id, entry);
149
163
  return entry;
@@ -163,20 +177,50 @@ export class UsageSourceRegistry {
163
177
  }
164
178
  this.cache.set(id, { at, entry });
165
179
  }
166
- errorEntry(source, id, error, label, fetchedAt = new Date(this.now()).toISOString()) {
167
- return {
168
- id,
169
- sourceId: source.id,
170
- sourceLabel: source.label,
171
- icon: source.icon,
172
- account: { label },
173
- fetchedAt,
174
- report: {
175
- status: "error",
176
- windows: [],
177
- error: error instanceof Error ? error.message : String(error),
178
- },
179
- };
180
+ async fetchWithFallback(known) {
181
+ let report = await this.fetchLogin(known.source, known.inputs[0]);
182
+ for (const input of known.inputs.slice(1)) {
183
+ if (report.status === "available")
184
+ break;
185
+ report = await this.fetchLogin(known.source, input);
186
+ }
187
+ return report;
188
+ }
189
+ async fetchLogin(source, input) {
190
+ try {
191
+ return UsageReportSchema.parse(await source.fetch(input));
192
+ }
193
+ catch (error) {
194
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
195
+ }
180
196
  }
181
197
  }
198
+ // The legacy RPC has only an error string; the current app owns its own presentation.
199
+ // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26.
200
+ function legacyProblem(problem, now) {
201
+ if (problem.kind === "no_quota")
202
+ return problem.detail;
203
+ const remedy = problem.refreshedBy
204
+ ? `Run ${problem.refreshedBy} to refresh it.`
205
+ : "Sign in again.";
206
+ if (problem.kind === "rejected")
207
+ return `Login rejected (HTTP ${problem.status}). ${remedy}`;
208
+ const elapsed = Math.max(0, now - Date.parse(problem.expiresAt));
209
+ let ago = "just now";
210
+ if (elapsed >= 86400000)
211
+ ago = `${Math.floor(elapsed / 86400000)}d ago`;
212
+ else if (elapsed >= 3600000)
213
+ ago = `${Math.floor(elapsed / 3600000)}h ago`;
214
+ else if (elapsed >= 60000)
215
+ ago = `${Math.floor(elapsed / 60000)}m ago`;
216
+ return `Login expired ${ago}. ${remedy}`;
217
+ }
218
+ // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26.
219
+ function legacyError(report, now) {
220
+ if (report.status === "error")
221
+ return report.error;
222
+ if (report.status === "unavailable")
223
+ return legacyProblem(report.problem, now);
224
+ return null;
225
+ }
182
226
  //# sourceMappingURL=index.js.map