@getpaseo/server 0.11.0-beta.1 → 0.11.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +131 -0
- package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +102 -50
- package/dist/server/builtin-plugins/claude-usage-source/shared/input.ts +13 -1
- package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +131 -0
- package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +70 -30
- package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +11 -1
- package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +28 -39
- package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
- package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
- package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
- package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
- package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
- package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
- package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
- package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
- package/dist/server/builtin-plugins/package.json +1 -1
- package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
- package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
- package/dist/server/server/orchestration-skills/internal/operations.js +19 -5
- package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
- package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
- package/dist/server/server/plugins/builtin/index.js +6 -0
- package/dist/server/server/plugins/compiler.js +4 -0
- package/dist/server/server/plugins/index.d.ts +18 -3
- package/dist/server/server/plugins/index.js +1 -2
- package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
- package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
- package/dist/server/server/plugins/plugin-process.js +2 -7
- package/dist/server/server/plugins/runtime.d.ts +0 -1
- package/dist/server/server/plugins/runtime.js +0 -11
- package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
- package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
- package/dist/server/server/plugins/usage-sources/index.d.ts +7 -9
- package/dist/server/server/plugins/usage-sources/index.js +98 -71
- package/dist/server/web-ui/_expo/static/js/web/{index-87a07caf197cc320e593332ed2d5e20f.js → index-1a3199cb049d45e47bfd4c216ce98838.js} +13 -13
- package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.gz +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.gz +0 -0
|
@@ -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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
}[]>;
|
|
@@ -24,7 +24,6 @@ export class PluginService {
|
|
|
24
24
|
this.errors = new Map();
|
|
25
25
|
this.listeners = new Set();
|
|
26
26
|
this.providers = new Map();
|
|
27
|
-
this.usageSources = new UsageSourceRegistry();
|
|
28
27
|
this.usageSourceIdsByPlugin = new Map();
|
|
29
28
|
this.providerIdsByPlugin = new Map();
|
|
30
29
|
this.providerListeners = new Set();
|
|
@@ -42,6 +41,7 @@ export class PluginService {
|
|
|
42
41
|
return request;
|
|
43
42
|
};
|
|
44
43
|
this.logger = logger.child({ module: "plugin-service" });
|
|
44
|
+
this.usageSources = new UsageSourceRegistry(Date.now, 300000, this.logger);
|
|
45
45
|
this.runtime =
|
|
46
46
|
dependencies.runtime ??
|
|
47
47
|
new PluginRuntime(logger, daemonVersion, {
|
|
@@ -477,7 +477,6 @@ export class PluginService {
|
|
|
477
477
|
throw new Error(`Invalid usage discovery from ${source.id}`);
|
|
478
478
|
return result;
|
|
479
479
|
},
|
|
480
|
-
identify: (input) => this.runtime.identifyUsage(pluginId, source.id, input),
|
|
481
480
|
fetch: (input) => {
|
|
482
481
|
return this.runtime.fetchUsage(pluginId, source.id, input);
|
|
483
482
|
},
|
|
@@ -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(
|
|
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.
|
|
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,27 +1,24 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
1
2
|
import { type ProviderUsage, type UsageReportEntry } from "@getpaseo/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,
|
|
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 readonly sources;
|
|
18
16
|
private readonly known;
|
|
19
17
|
private readonly cache;
|
|
20
18
|
private readonly pending;
|
|
21
|
-
constructor(now?: () => number, ttlMs?: number);
|
|
19
|
+
constructor(now?: () => number, ttlMs?: number, logger?: Pick<Logger, "warn">);
|
|
22
20
|
register(source: UsageSource): void;
|
|
23
21
|
unregister(id: string): void;
|
|
24
|
-
private identify;
|
|
25
22
|
listReports(options?: {
|
|
26
23
|
forceRefresh?: boolean;
|
|
27
24
|
reportIds?: string[];
|
|
@@ -33,6 +30,7 @@ export declare class UsageSourceRegistry {
|
|
|
33
30
|
}>;
|
|
34
31
|
private fetchId;
|
|
35
32
|
private writeCache;
|
|
36
|
-
private
|
|
33
|
+
private fetchWithFallback;
|
|
34
|
+
private fetchLogin;
|
|
37
35
|
}
|
|
38
36
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { UsageReportSchema, } from "@getpaseo/protocol/messages";
|
|
2
|
-
/** Owns account identity,
|
|
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.sources = new Map();
|
|
8
10
|
this.known = new Map();
|
|
9
11
|
this.cache = new Map();
|
|
@@ -23,44 +25,49 @@ export class UsageSourceRegistry {
|
|
|
23
25
|
if (key.startsWith(`${id}:`))
|
|
24
26
|
this.cache.delete(key);
|
|
25
27
|
}
|
|
26
|
-
async identify(source, input) {
|
|
27
|
-
try {
|
|
28
|
-
const account = await source.identify(input);
|
|
29
|
-
if (account === null)
|
|
30
|
-
return null;
|
|
31
|
-
if (typeof account !== "object" ||
|
|
32
|
-
(account.label !== undefined && typeof account.label !== "string"))
|
|
33
|
-
throw new Error(`Invalid account identity from ${source.id}`);
|
|
34
|
-
if (typeof account.key !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(account.key))
|
|
35
|
-
throw new Error(`Invalid account key from ${source.id}`);
|
|
36
|
-
const id = `${source.id}:${account.key}`;
|
|
37
|
-
this.known.set(id, { source, input, label: account.label });
|
|
38
|
-
return id;
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
const id = `${source.id}:!error`;
|
|
42
|
-
this.writeCache(id, this.errorEntry(source, id, error));
|
|
43
|
-
return id;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
28
|
async listReports(options = {}) {
|
|
47
29
|
const ids = options.reportIds ?? (await this.discoverReportIds());
|
|
48
|
-
return Promise.all([...new Set(ids)]
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
return Promise.all([...new Set(ids)].flatMap((id) => {
|
|
31
|
+
const known = this.known.get(id);
|
|
32
|
+
return known ? [this.fetchId(id, known, options.forceRefresh)] : [];
|
|
33
|
+
}));
|
|
51
34
|
}
|
|
52
35
|
async discoverReportIds() {
|
|
53
36
|
const discovered = await Promise.all([...this.sources.values()].map(async (source) => {
|
|
54
37
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
38
|
+
const accounts = z
|
|
39
|
+
.array(z.object({
|
|
40
|
+
key: z.string().regex(/^[A-Za-z0-9._-]{1,128}$/),
|
|
41
|
+
label: z.string().optional(),
|
|
42
|
+
input: z.json(),
|
|
43
|
+
}))
|
|
44
|
+
.parse(await source.discover());
|
|
45
|
+
const knownReports = new Map();
|
|
46
|
+
const ids = [];
|
|
47
|
+
for (const account of accounts) {
|
|
48
|
+
const id = `${source.id}:${account.key}`;
|
|
49
|
+
ids.push(id);
|
|
50
|
+
const known = knownReports.get(id);
|
|
51
|
+
if (known)
|
|
52
|
+
known.inputs.push(account.input);
|
|
53
|
+
else
|
|
54
|
+
knownReports.set(id, { source, inputs: [account.input], label: account.label });
|
|
55
|
+
}
|
|
56
|
+
for (const key of this.known.keys()) {
|
|
57
|
+
if (key.startsWith(`${source.id}:`))
|
|
58
|
+
this.known.delete(key);
|
|
59
|
+
}
|
|
60
|
+
for (const [id, known] of knownReports)
|
|
61
|
+
this.known.set(id, known);
|
|
62
|
+
return ids;
|
|
59
63
|
}
|
|
60
64
|
catch (error) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
for (const key of this.known.keys()) {
|
|
66
|
+
if (key.startsWith(`${source.id}:`))
|
|
67
|
+
this.known.delete(key);
|
|
68
|
+
}
|
|
69
|
+
this.logger.warn({ sourceId: source.id, err: error }, "Usage source discovery failed");
|
|
70
|
+
return [];
|
|
64
71
|
}
|
|
65
72
|
}));
|
|
66
73
|
return discovered.flat();
|
|
@@ -78,41 +85,31 @@ export class UsageSourceRegistry {
|
|
|
78
85
|
? `${entry.sourceLabel} (${entry.account.label})`
|
|
79
86
|
: entry.sourceLabel,
|
|
80
87
|
status: entry.report.status,
|
|
81
|
-
planLabel: entry.report.planLabel ?? null,
|
|
82
|
-
windows: entry.report.windows,
|
|
83
|
-
balances: entry.report.balances ?? [],
|
|
84
|
-
details: entry.report.details ?? [],
|
|
85
|
-
error: entry.report.
|
|
88
|
+
planLabel: entry.report.status === "available" ? (entry.report.planLabel ?? null) : null,
|
|
89
|
+
windows: entry.report.status === "available" ? entry.report.windows : [],
|
|
90
|
+
balances: entry.report.status === "available" ? (entry.report.balances ?? []) : [],
|
|
91
|
+
details: entry.report.status === "available" ? (entry.report.details ?? []) : [],
|
|
92
|
+
error: legacyError(entry.report, this.now()),
|
|
86
93
|
})),
|
|
87
94
|
};
|
|
88
95
|
}
|
|
89
|
-
fetchId(id, forceRefresh = false) {
|
|
90
|
-
const known = this.known.get(id);
|
|
96
|
+
fetchId(id, known, forceRefresh = false) {
|
|
91
97
|
const cached = this.cache.get(id);
|
|
92
|
-
if (!known)
|
|
93
|
-
return Promise.resolve(cached.entry);
|
|
94
98
|
if (!forceRefresh && cached && this.now() - cached.at < this.ttlMs)
|
|
95
99
|
return Promise.resolve(cached.entry);
|
|
96
100
|
const pending = this.pending.get(id);
|
|
97
101
|
if (pending)
|
|
98
102
|
return pending;
|
|
99
103
|
const request = (async () => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
fetchedAt: new Date(this.now()).toISOString(),
|
|
110
|
-
report,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
entry = this.errorEntry(known.source, id, error, known.label);
|
|
115
|
-
}
|
|
104
|
+
const entry = {
|
|
105
|
+
id,
|
|
106
|
+
sourceId: known.source.id,
|
|
107
|
+
sourceLabel: known.source.label,
|
|
108
|
+
icon: known.source.icon,
|
|
109
|
+
account: { label: known.label },
|
|
110
|
+
fetchedAt: new Date(this.now()).toISOString(),
|
|
111
|
+
report: await this.fetchWithFallback(known),
|
|
112
|
+
};
|
|
116
113
|
this.writeCache(id, entry);
|
|
117
114
|
return entry;
|
|
118
115
|
})();
|
|
@@ -131,20 +128,50 @@ export class UsageSourceRegistry {
|
|
|
131
128
|
}
|
|
132
129
|
this.cache.set(id, { at, entry });
|
|
133
130
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
async fetchWithFallback(known) {
|
|
132
|
+
let report = await this.fetchLogin(known.source, known.inputs[0]);
|
|
133
|
+
for (const input of known.inputs.slice(1)) {
|
|
134
|
+
if (report.status === "available")
|
|
135
|
+
break;
|
|
136
|
+
report = await this.fetchLogin(known.source, input);
|
|
137
|
+
}
|
|
138
|
+
return report;
|
|
139
|
+
}
|
|
140
|
+
async fetchLogin(source, input) {
|
|
141
|
+
try {
|
|
142
|
+
return UsageReportSchema.parse(await source.fetch(input));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return { status: "error", error: error instanceof Error ? error.message : String(error) };
|
|
146
|
+
}
|
|
148
147
|
}
|
|
149
148
|
}
|
|
149
|
+
// The legacy RPC has only an error string; the current app owns its own presentation.
|
|
150
|
+
// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26.
|
|
151
|
+
function legacyProblem(problem, now) {
|
|
152
|
+
if (problem.kind === "no_quota")
|
|
153
|
+
return problem.detail;
|
|
154
|
+
const remedy = problem.refreshedBy
|
|
155
|
+
? `Run ${problem.refreshedBy} to refresh it.`
|
|
156
|
+
: "Sign in again.";
|
|
157
|
+
if (problem.kind === "rejected")
|
|
158
|
+
return `Login rejected (HTTP ${problem.status}). ${remedy}`;
|
|
159
|
+
const elapsed = Math.max(0, now - Date.parse(problem.expiresAt));
|
|
160
|
+
let ago = "just now";
|
|
161
|
+
if (elapsed >= 86400000)
|
|
162
|
+
ago = `${Math.floor(elapsed / 86400000)}d ago`;
|
|
163
|
+
else if (elapsed >= 3600000)
|
|
164
|
+
ago = `${Math.floor(elapsed / 3600000)}h ago`;
|
|
165
|
+
else if (elapsed >= 60000)
|
|
166
|
+
ago = `${Math.floor(elapsed / 60000)}m ago`;
|
|
167
|
+
return `Login expired ${ago}. ${remedy}`;
|
|
168
|
+
}
|
|
169
|
+
// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26.
|
|
170
|
+
function legacyError(report, now) {
|
|
171
|
+
if (report.status === "error")
|
|
172
|
+
return report.error;
|
|
173
|
+
if (report.status === "unavailable")
|
|
174
|
+
return legacyProblem(report.problem, now);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
150
177
|
//# sourceMappingURL=index.js.map
|