dsh-plugin-usage 0.4.0 → 0.5.0
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/README.md +101 -9
- package/image-1.png +0 -0
- package/image.png +0 -0
- package/lib/client/index.js +1566 -99
- package/lib/host/fold-cache.js +117 -0
- package/lib/host/fold.js +11 -0
- package/lib/host/holidays.js +104 -0
- package/lib/host/index.js +335 -30
- package/lib/host/peak.js +107 -0
- package/lib/host/prices.js +876 -0
- package/lib/host/pricing.js +349 -0
- package/lib/host/query.js +60 -2
- package/lib/host/rpc.js +69 -36
- package/lib/host/scan.js +231 -12
- package/lib/host/settings.js +119 -76
- package/lib/host/state.js +18 -2
- package/lib/host/typert.js +221 -0
- package/package.json +11 -5
- package/screenshots.json +4 -0
- package/lib/host/client-source.js +0 -34
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native client→host channel: a typert Remote contribution.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists (xshared-lib experience ⓪, "原生优先"): the plugin used to
|
|
5
|
+
* invent its own envelope on top of `connection.fetch`. That registry is native,
|
|
6
|
+
* but the typed Remote channel is the one the rest of the harness speaks, and a
|
|
7
|
+
* third party can join it without compiling anything — `ctx.typert.register()`
|
|
8
|
+
* is a runtime API ("any other owner calls ctx.typert.register(contribution)
|
|
9
|
+
* directly", @deepseek-ai/dsh-typert-registry README).
|
|
10
|
+
*
|
|
11
|
+
* What the harness demands of a contribution (read from the shipped distribution,
|
|
12
|
+
* @deepseek-ai/dsh-typert-loader `validateTypertManifest` / `requireInvocation` /
|
|
13
|
+
* `requireStrictCodec`, and @deepseek-ai/dsh-typert-registry `register`):
|
|
14
|
+
*
|
|
15
|
+
* · `package` names the owner; the manifest must be owned by the package that
|
|
16
|
+
* declares it;
|
|
17
|
+
* · every invocation carries `id/service/namespace/method`;
|
|
18
|
+
* · `namespace` MUST be the package name: the client-side service key is
|
|
19
|
+
* `remote.<namespace>` (@deepseek-ai/dsh-api-gateway client `remoteServiceKey`),
|
|
20
|
+
* so an endpoint name here means a service nobody can reach;
|
|
21
|
+
* · every parameter is `source: 'json'` or `'lookup'`, and each parameter and
|
|
22
|
+
* the result must be a STRICT codec (`mode: 'strict'`, non-empty
|
|
23
|
+
* `typeSymbol`, a `create()` factory; give `encode` on the result or the
|
|
24
|
+
* gateway skips its own result handling);
|
|
25
|
+
* · the gateway resolves `descriptor.service` on the receiver context and
|
|
26
|
+
* cross-checks the `typertRemote` binding, so the plugin must ALSO provide a
|
|
27
|
+
* service of that name whose binding says `service` (itself), `serviceKey`
|
|
28
|
+
* and `namespace` (both the package name).
|
|
29
|
+
*
|
|
30
|
+
* Everything here is feature-detected: a composition without `typert` (or
|
|
31
|
+
* without a reflect `provide`) keeps using the Fetch routes in rpc.js. A
|
|
32
|
+
* missing capability must never throw during activation.
|
|
33
|
+
*/
|
|
34
|
+
/** The service key, the wire namespace and the package name — one string. */
|
|
35
|
+
export const USAGE_SERVICE = 'dsh-plugin-usage';
|
|
36
|
+
/** Wire names the registry accepts for namespace/method (`validateWireName`). */
|
|
37
|
+
const WIRE_NAME = /^[A-Za-z0-9_$.-]+$/;
|
|
38
|
+
/** A schema projection: the gateway only calls `parse` on inputs. */
|
|
39
|
+
const ANY_SCHEMA = Object.freeze({
|
|
40
|
+
parse(value) {
|
|
41
|
+
return value;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const argsCodec = (endpoint) => ({
|
|
45
|
+
mode: 'strict',
|
|
46
|
+
typeSymbol: `${USAGE_SERVICE}/args/${endpoint}`,
|
|
47
|
+
create: () => ANY_SCHEMA,
|
|
48
|
+
});
|
|
49
|
+
const resultCodec = (endpoint) => ({
|
|
50
|
+
mode: 'strict',
|
|
51
|
+
typeSymbol: `${USAGE_SERVICE}/result/${endpoint}`,
|
|
52
|
+
create: () => ANY_SCHEMA,
|
|
53
|
+
encode: (value) => value,
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* One InvocationDescriptor for an endpoint.
|
|
57
|
+
*
|
|
58
|
+
* One positional `args` field, because the gateway calls Remote methods with
|
|
59
|
+
* positional arguments: the client passes exactly one JSON object.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} endpoint - endpoint name such as `query`.
|
|
62
|
+
* @returns {object} invocation descriptor.
|
|
63
|
+
*/
|
|
64
|
+
export function buildInvocation(endpoint) {
|
|
65
|
+
return {
|
|
66
|
+
id: `${USAGE_SERVICE}#${USAGE_SERVICE}/${endpoint}`,
|
|
67
|
+
service: USAGE_SERVICE,
|
|
68
|
+
namespace: USAGE_SERVICE,
|
|
69
|
+
method: endpoint,
|
|
70
|
+
invocation: { kind: 'direct' },
|
|
71
|
+
parameters: [{ name: 'args', wire: 'args', source: 'json', codec: argsCodec(endpoint) }],
|
|
72
|
+
result: resultCodec(endpoint),
|
|
73
|
+
sourceLocation: { file: 'src/host/typert.js', line: 1, column: 1 },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The contribution handed to `ctx.typert.register()`.
|
|
78
|
+
*
|
|
79
|
+
* @param {string[]} endpoints - endpoint names to expose.
|
|
80
|
+
* @returns {object} typert contribution.
|
|
81
|
+
*/
|
|
82
|
+
export function typertContribution(endpoints) {
|
|
83
|
+
if (!Array.isArray(endpoints) || endpoints.length === 0) {
|
|
84
|
+
throw new Error('typert contribution needs at least one endpoint');
|
|
85
|
+
}
|
|
86
|
+
for (const endpoint of endpoints) {
|
|
87
|
+
if (typeof endpoint !== 'string' || !WIRE_NAME.test(endpoint)) {
|
|
88
|
+
throw new Error('endpoint name is not wire-safe: ' + String(endpoint));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
package: USAGE_SERVICE,
|
|
93
|
+
face: 'host',
|
|
94
|
+
schemas: [],
|
|
95
|
+
model: { services: [], events: [], objects: [] },
|
|
96
|
+
invocations: endpoints.map(buildInvocation),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The receiver service: the gateway resolves `descriptor.service` on it and
|
|
101
|
+
* checks the binding before calling a method.
|
|
102
|
+
*
|
|
103
|
+
* Methods are keyed by the endpoint name verbatim (hyphens are fine as keys);
|
|
104
|
+
* the browser half also tries the camelCase spelling, so both work.
|
|
105
|
+
*
|
|
106
|
+
* @param {Map<string, (args: any) => any>} handlers - endpoint handlers.
|
|
107
|
+
* @returns {object} service instance with a frozen `typertRemote` binding.
|
|
108
|
+
*/
|
|
109
|
+
export function createReceiver(handlers) {
|
|
110
|
+
const receiver = {};
|
|
111
|
+
for (const [endpoint, fn] of handlers) {
|
|
112
|
+
receiver[endpoint] = (args) => fn(args === undefined || args === null ? {} : args);
|
|
113
|
+
}
|
|
114
|
+
receiver.typertRemote = Object.freeze({
|
|
115
|
+
service: receiver,
|
|
116
|
+
serviceKey: USAGE_SERVICE,
|
|
117
|
+
namespace: USAGE_SERVICE,
|
|
118
|
+
});
|
|
119
|
+
return receiver;
|
|
120
|
+
}
|
|
121
|
+
/** Read a service without tripping the strict ctx proxy. */
|
|
122
|
+
function serviceOf(ctx, name) {
|
|
123
|
+
if (!ctx)
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
if (typeof ctx.get === 'function') {
|
|
127
|
+
const found = ctx.get(name);
|
|
128
|
+
if (found)
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* not injected */
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return ctx[name] ?? null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The service-providing entry point.
|
|
144
|
+
*
|
|
145
|
+
* `ctx.provide(name, value, check)` is Context's own API (cordis `Context.provide`,
|
|
146
|
+
* "register a service implementation owned by the current fiber", returns a
|
|
147
|
+
* disposer). `ctx.reflect.provide` is the same store reached through the reflect
|
|
148
|
+
* facade, kept as a fallback for compositions that hide the direct method.
|
|
149
|
+
*/
|
|
150
|
+
function providerOf(ctx) {
|
|
151
|
+
if (!ctx)
|
|
152
|
+
return null;
|
|
153
|
+
try {
|
|
154
|
+
if (typeof ctx.provide === 'function')
|
|
155
|
+
return (name, value) => ctx.provide(name, value);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* not exposed */
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const reflect = ctx.reflect;
|
|
162
|
+
if (reflect && typeof reflect.provide === 'function')
|
|
163
|
+
return (name, value) => reflect.provide(name, value);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* not exposed */
|
|
167
|
+
}
|
|
168
|
+
return serviceOf(ctx, 'reflect');
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Join the native Remote channel, when this composition has one.
|
|
172
|
+
*
|
|
173
|
+
* Returns a small record rather than a boolean: the plugin reports which channel
|
|
174
|
+
* it ended up on, and WHY when it could not take the native one. That record
|
|
175
|
+
* travels to the browser with the dashboard response (`channel`), because a
|
|
176
|
+
* plugin logger is not always wired into the harness log.
|
|
177
|
+
*
|
|
178
|
+
* @param {any} ctx - host plugin context.
|
|
179
|
+
* @param {Map<string, (args: any) => any>} handlers - endpoint handlers.
|
|
180
|
+
* @param {(message: string, level?: string) => void} [log] - log sink.
|
|
181
|
+
* @returns {{ ok: boolean, transport: 'remote' | 'fetch', reason: string }} decision.
|
|
182
|
+
*/
|
|
183
|
+
export function registerTypert(ctx, handlers, log) {
|
|
184
|
+
const note = (message, level = 'warn') => { if (typeof log === 'function')
|
|
185
|
+
log(message, level); };
|
|
186
|
+
const failed = (reason) => {
|
|
187
|
+
note('[usage] client→host channel: Fetch (' + reason + ')');
|
|
188
|
+
return { ok: false, transport: 'fetch', reason };
|
|
189
|
+
};
|
|
190
|
+
const typert = serviceOf(ctx, 'typert');
|
|
191
|
+
if (typert === null || typeof typert.register !== 'function') {
|
|
192
|
+
return failed('no typert registry in this composition');
|
|
193
|
+
}
|
|
194
|
+
const provider = providerOf(ctx);
|
|
195
|
+
if (provider === null || typeof provider !== 'function') {
|
|
196
|
+
return failed('this ctx cannot provide a service (ctx.provide missing)');
|
|
197
|
+
}
|
|
198
|
+
let dispose = null;
|
|
199
|
+
try {
|
|
200
|
+
dispose = provider(USAGE_SERVICE, createReceiver(handlers));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return failed('providing the receiver failed: ' + String((error && error.message) || error));
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
typert.register(typertContribution([...handlers.keys()]));
|
|
207
|
+
note('[usage] client→host channel: native typert Remote (' + handlers.size + ' endpoints)', 'info');
|
|
208
|
+
return { ok: true, transport: 'remote', reason: 'registered ' + handlers.size + ' endpoints' };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// Registration is atomic: a rejected contribution means nothing is reachable,
|
|
212
|
+
// so take the receiver back out and let the Fetch routes carry the plugin.
|
|
213
|
+
if (typeof dispose === 'function') {
|
|
214
|
+
try {
|
|
215
|
+
dispose();
|
|
216
|
+
}
|
|
217
|
+
catch { /* already gone */ }
|
|
218
|
+
}
|
|
219
|
+
return failed('typert rejected the contribution: ' + String((error && error.message) || error));
|
|
220
|
+
}
|
|
221
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-usage",
|
|
3
3
|
"description": "Token usage statistics for DeepSeek Harness — daily/custom-range views, model filters, call counts and cost, inspired by CC Switch statistics.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"icon.svg",
|
|
28
28
|
"locale",
|
|
29
29
|
"image.png",
|
|
30
|
-
"image-1.png"
|
|
30
|
+
"image-1.png",
|
|
31
|
+
"screenshots.json"
|
|
31
32
|
],
|
|
32
33
|
"keywords": [
|
|
33
34
|
"dsh",
|
|
@@ -40,10 +41,11 @@
|
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=22",
|
|
43
|
-
"dsh": ">=0.2.
|
|
44
|
+
"dsh": ">=0.2.1-rc.3 <0.3.0"
|
|
44
45
|
},
|
|
45
46
|
"dsh": {
|
|
46
47
|
"manifestVersion": 1,
|
|
48
|
+
"verifiedAgainst": "0.2.1-rc.4",
|
|
47
49
|
"bundle": {
|
|
48
50
|
"patch": "./cordis.patch.yml"
|
|
49
51
|
},
|
|
@@ -71,8 +73,12 @@
|
|
|
71
73
|
},
|
|
72
74
|
"scripts": {
|
|
73
75
|
"build": "tsc -p tsconfig.json && node scripts/build-client.js",
|
|
74
|
-
"test": "node --test tests/*.test.js",
|
|
76
|
+
"test": "node --test tests/*.test.js tests/*.test.cjs",
|
|
77
|
+
"prices": "node scripts/gen-prices.mjs",
|
|
75
78
|
"watch": "tsc -p tsconfig.json --watch",
|
|
76
|
-
"prepublishOnly": "npm run build && npm test"
|
|
79
|
+
"prepublishOnly": "npm run build && npm test && npm run verify:typert && npm run verify:remote",
|
|
80
|
+
"pretest": "npm run build",
|
|
81
|
+
"verify:typert": "node scripts/verify-typert.mjs",
|
|
82
|
+
"verify:remote": "node scripts/verify-remote-channel.mjs"
|
|
77
83
|
}
|
|
78
84
|
}
|
package/screenshots.json
ADDED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client source loader.
|
|
3
|
-
*
|
|
4
|
-
* The full dashboard UI lives in src/client/index.js (single source of
|
|
5
|
-
* truth). A dynamic plugin cannot import files, so the host reads the file
|
|
6
|
-
* and serves it to the browser half, which evaluates it with new Function.
|
|
7
|
-
*
|
|
8
|
-
* P1-4: candidates are derived from import.meta.url so the dynamic-plugin
|
|
9
|
-
* dev flow works from any checkout location.
|
|
10
|
-
*/
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
const CLIENT_SOURCE_CANDIDATES = [
|
|
13
|
-
// src/host -> src/client in dev; lib/host -> lib/client when built.
|
|
14
|
-
fileURLToPath(new URL('../client/index.js', import.meta.url)),
|
|
15
|
-
// Repo source fallback when the built lib runs inside a checkout.
|
|
16
|
-
fileURLToPath(new URL('../../src/client/index.js', import.meta.url)),
|
|
17
|
-
];
|
|
18
|
-
export async function getClientSource(ctx) {
|
|
19
|
-
const fs = ctx.get('fs');
|
|
20
|
-
if (fs === undefined)
|
|
21
|
-
return { ok: false, error: 'fs service unavailable' };
|
|
22
|
-
for (const p of CLIENT_SOURCE_CANDIDATES) {
|
|
23
|
-
try {
|
|
24
|
-
const target = await fs.resolve(p);
|
|
25
|
-
const body = await fs.readText(target);
|
|
26
|
-
if (typeof body === 'string' && body.length > 0)
|
|
27
|
-
return { ok: true, body };
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
// try the next candidate
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return { ok: false, error: 'client source not found (tried: ' + CLIENT_SOURCE_CANDIDATES.join(', ') + ')' };
|
|
34
|
-
}
|