clay-server 5.2.0-beta.9 → 5.2.1-beta.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.
- package/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +63 -24
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +5 -1
- package/lib/public/app.js +30 -18
- package/lib/public/css/command-palette.css +12 -2
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +4 -5
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/messages.css +30 -11
- package/lib/public/css/overlays.css +67 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/right-workbench.css +28 -0
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/title-bar.css +99 -0
- package/lib/public/index.html +54 -66
- package/lib/public/modules/account-transport-settings.js +8 -2
- package/lib/public/modules/admin-settings-email.js +7 -2
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-projects.js +1 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/contextual-clay.js +3 -1
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/home-surface-boot.js +0 -1
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/pair-result-status.js +89 -39
- package/lib/public/modules/pane-appearance-bridge.js +59 -0
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings-environment.js +12 -1
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +32 -44
- package/lib/public/modules/projects-hub.js +39 -28
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/theme.js +31 -3
- package/lib/public/modules/topbar-workbench-controls.js +102 -0
- package/lib/public/modules/user-settings-integrations.js +2 -1
- package/lib/public/modules/user-settings-layout.js +12 -34
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +323 -0
- package/lib/public/style.css +13 -10
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +11 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +12 -2
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +2 -1
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Owner-scoped vendor usage overview for the top bar.
|
|
2
|
+
//
|
|
3
|
+
// This is a read model over quota Clay already observed from the owner's own
|
|
4
|
+
// sessions in this project. It is separate from the active-session rejection,
|
|
5
|
+
// warning and scheduling path and never changes it.
|
|
6
|
+
//
|
|
7
|
+
// Identity boundary: Clay cannot prove which vendor account a session used, so
|
|
8
|
+
// observations are never merged into a synthetic account view. Each observing
|
|
9
|
+
// session is its own source; the overview shows the single most recent source's
|
|
10
|
+
// coherent snapshot and reports which session it came from and how many other
|
|
11
|
+
// sources exist. A different account in a later session simply becomes the
|
|
12
|
+
// newest source; earlier buckets are never combined with it. Sources whose
|
|
13
|
+
// session was deleted, replaced or no longer belongs to the owner are ignored.
|
|
14
|
+
//
|
|
15
|
+
// Account figures come from vendor-usage-service.js, which reads a vendor's own
|
|
16
|
+
// native source only when the credential provably belongs to the requester, and
|
|
17
|
+
// are reported beside, never merged into, the session-observed snapshot. Nothing
|
|
18
|
+
// here starts an agent turn or invents a value: a vendor with no observation is
|
|
19
|
+
// reported with no windows and no observation time, and a vendor without a
|
|
20
|
+
// verified source says so (capability.mode "unsupported" plus the reason).
|
|
21
|
+
|
|
22
|
+
var usage = require("./yoke/usage");
|
|
23
|
+
var vendorRegistry = require("./yoke/vendor-registry");
|
|
24
|
+
var quotaSnapshots = require("./sdk-quota-snapshot");
|
|
25
|
+
var capabilities = require("./vendor-usage-capabilities");
|
|
26
|
+
var usageService = require("./vendor-usage-service");
|
|
27
|
+
|
|
28
|
+
var HISTORY_SCAN_LIMIT = 2000;
|
|
29
|
+
var DEFAULT_OWNER = "default";
|
|
30
|
+
|
|
31
|
+
var MAX_SOURCES = 24;
|
|
32
|
+
|
|
33
|
+
function attachVendorUsageOverview(ctx) {
|
|
34
|
+
var slug = ctx.slug;
|
|
35
|
+
var sm = ctx.sm;
|
|
36
|
+
var usersModule = ctx.usersModule;
|
|
37
|
+
var clients = ctx.clients;
|
|
38
|
+
var sendTo = ctx.sendTo;
|
|
39
|
+
var now = typeof ctx.now === "function" ? ctx.now : Date.now;
|
|
40
|
+
var service = ctx.usageService || usageService.createUsageService({
|
|
41
|
+
now: now, multiUser: multiUser, osUsersEnabled: function () { return !!ctx.osUsers; },
|
|
42
|
+
getAdapter: function (vendor) { return ctx.adapters ? ctx.adapters[vendor] || null : null; },
|
|
43
|
+
sourceLive: function (session) { return !!(sm && sm.sessions && typeof sm.sessions.get === "function" && sm.sessions.get(session.localId) === session); },
|
|
44
|
+
});
|
|
45
|
+
var owners = Object.create(null);
|
|
46
|
+
var seeded = Object.create(null);
|
|
47
|
+
var sequence = 0;
|
|
48
|
+
|
|
49
|
+
function multiUser() {
|
|
50
|
+
return !!(usersModule && usersModule.isMultiUser && usersModule.isMultiUser());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Session owner key. In account mode an ownerless legacy session belongs to
|
|
54
|
+
// nobody we can name, so it contributes to no one's overview.
|
|
55
|
+
function sessionOwner(session) {
|
|
56
|
+
if (!multiUser()) return DEFAULT_OWNER;
|
|
57
|
+
return session && session.ownerId ? String(session.ownerId) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The caller's live identity: the cached socket user must still exist and
|
|
61
|
+
// still have access to this project. Returns the owner key or null.
|
|
62
|
+
function socketOwner(ws) {
|
|
63
|
+
if (!ws || ws.readyState !== 1 || (clients && typeof clients.has === "function" && !clients.has(ws))) return null;
|
|
64
|
+
if (!multiUser()) return DEFAULT_OWNER;
|
|
65
|
+
var user = ws._clayUser;
|
|
66
|
+
if (!user || !user.id || typeof usersModule.findUserById !== "function" || !usersModule.findUserById(user.id)) return null;
|
|
67
|
+
if (typeof usersModule.canAccessProject !== "function" || typeof ctx.getProjectAccess !== "function") return null;
|
|
68
|
+
if (usersModule.canAccessProject(user.id, ctx.getProjectAccess()) !== true) return null;
|
|
69
|
+
return String(user.id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function liveSession(owner, vendor, source) {
|
|
73
|
+
var session = sm && sm.sessions && typeof sm.sessions.get === "function" ? sm.sessions.get(source.sessionId) : null;
|
|
74
|
+
if (!session || session !== source.session || sessionOwner(session) !== owner || (session.vendor || "claude") !== vendor) return null;
|
|
75
|
+
return session;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Sources keep the exact session object they were recorded for, so a session
|
|
79
|
+
// later replaced at the same id never inherits them.
|
|
80
|
+
function sourceFor(owner, vendor, session) {
|
|
81
|
+
var byVendor = owners[owner] || (owners[owner] = Object.create(null));
|
|
82
|
+
var entry = byVendor[vendor] || (byVendor[vendor] = { sources: Object.create(null) });
|
|
83
|
+
var key = String(session.localId);
|
|
84
|
+
var source = entry.sources[key];
|
|
85
|
+
if (source && source.session !== session) { delete entry.sources[key]; source = null; }
|
|
86
|
+
if (!source) source = entry.sources[key] = { session: session, sessionId: session.localId, buckets: Object.create(null), lastObservedAtMs: null, seq: 0 };
|
|
87
|
+
return { entry: entry, source: source };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Ordering is lexicographic on (observation time, sequence); never arithmetic,
|
|
91
|
+
// so large timestamps cannot lose precision.
|
|
92
|
+
function compareSources(a, b) {
|
|
93
|
+
var at = Number.isFinite(a.lastObservedAtMs) ? a.lastObservedAtMs : -Infinity;
|
|
94
|
+
var bt = Number.isFinite(b.lastObservedAtMs) ? b.lastObservedAtMs : -Infinity;
|
|
95
|
+
if (at !== bt) return at < bt ? -1 : 1;
|
|
96
|
+
return a.seq - b.seq;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Runs after a successful update, so the newest source can never evict itself.
|
|
100
|
+
function trimSources(entry) {
|
|
101
|
+
var keys = Object.keys(entry.sources);
|
|
102
|
+
if (keys.length <= MAX_SOURCES) return;
|
|
103
|
+
keys.sort(function (x, y) { return compareSources(entry.sources[x], entry.sources[y]); });
|
|
104
|
+
for (var i = 0; i < keys.length - MAX_SOURCES; i++) delete entry.sources[keys[i]];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function newer(existing, observedAtMs) {
|
|
108
|
+
if (!existing) return true;
|
|
109
|
+
if (!Number.isFinite(observedAtMs)) return !Number.isFinite(existing.observedAtMs);
|
|
110
|
+
return !Number.isFinite(existing.observedAtMs) || observedAtMs >= existing.observedAtMs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function numberOrNull(value) {
|
|
114
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function touch(source, observedAtMs) {
|
|
118
|
+
source.seq = ++sequence;
|
|
119
|
+
if (Number.isFinite(observedAtMs) && (source.lastObservedAtMs === null || observedAtMs > source.lastObservedAtMs)) source.lastObservedAtMs = observedAtMs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Returns true when the overview changed.
|
|
123
|
+
function apply(owner, vendor, session, message, observedAtMs, origin) {
|
|
124
|
+
if (!owner || !vendor || !message || !session) return false;
|
|
125
|
+
var event = usage.clientEvent(message, vendor);
|
|
126
|
+
if (!event || event.ignored === true) return false;
|
|
127
|
+
var held = sourceFor(owner, vendor, session);
|
|
128
|
+
var source = held.source;
|
|
129
|
+
var at = Number.isFinite(observedAtMs) ? observedAtMs : null;
|
|
130
|
+
var changed = applyToSource(source, event, at, origin);
|
|
131
|
+
if (changed) trimSources(held.entry);
|
|
132
|
+
else if (Object.keys(source.buckets).length === 0) delete held.entry.sources[String(session.localId)];
|
|
133
|
+
return changed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function applyToSource(source, event, at, origin) {
|
|
137
|
+
if (event.source === "account_snapshot" && Array.isArray(event.quotaWindows)) {
|
|
138
|
+
var limitId = String(event.limitId || "");
|
|
139
|
+
var guardKey = "snapshot\u0000" + limitId + "\u0000guard";
|
|
140
|
+
var guard = source.buckets[guardKey];
|
|
141
|
+
if (guard && !newer(guard, at)) return false;
|
|
142
|
+
Object.keys(source.buckets).forEach(function (key) {
|
|
143
|
+
if (source.buckets[key].limitId === limitId && source.buckets[key].kind === "snapshot") delete source.buckets[key];
|
|
144
|
+
});
|
|
145
|
+
source.buckets[guardKey] = { kind: "guard", observedAtMs: at, limitId: limitId };
|
|
146
|
+
event.quotaWindows.forEach(function (window) {
|
|
147
|
+
source.buckets["snapshot\u0000" + limitId + "\u0000" + window.key] = {
|
|
148
|
+
kind: "snapshot", limitId: limitId, key: window.key,
|
|
149
|
+
windowDurationMins: numberOrNull(window.windowDurationMins),
|
|
150
|
+
usedPercent: numberOrNull(window.usedPercent),
|
|
151
|
+
resetsAt: numberOrNull(window.resetsAt),
|
|
152
|
+
observedAtMs: at, origin: origin,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
touch(source, at);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (event.type === "rate_limit_usage" && event.rateLimitType && numberOrNull(event.resetsAt)) {
|
|
159
|
+
var resetKey = "reset\u0000" + event.rateLimitType;
|
|
160
|
+
var prior = source.buckets[resetKey];
|
|
161
|
+
if (!newer(prior, at)) return false;
|
|
162
|
+
source.buckets[resetKey] = {
|
|
163
|
+
kind: "reset", rateLimitType: event.rateLimitType, resetsAt: event.resetsAt,
|
|
164
|
+
status: typeof event.status === "string" ? event.status : null,
|
|
165
|
+
usedPercent: prior && prior.resetsAt === event.resetsAt ? prior.usedPercent : null,
|
|
166
|
+
observedAtMs: at, origin: origin,
|
|
167
|
+
};
|
|
168
|
+
touch(source, at);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (event.type === "rate_limit" && event.rateLimitType && numberOrNull(event.utilization) !== null) {
|
|
172
|
+
var bucket = source.buckets["reset\u0000" + event.rateLimitType];
|
|
173
|
+
// Utilization is only attached to a window whose reset time is known.
|
|
174
|
+
if (bucket && newer(bucket, at)) {
|
|
175
|
+
bucket.usedPercent = Math.round(event.utilization * 1000) / 10;
|
|
176
|
+
bucket.observedAtMs = at;
|
|
177
|
+
touch(source, at);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Seed once per owner from session history that survived a restart. Observed
|
|
185
|
+
// times come from the record's own stamp, never from the time of the scan.
|
|
186
|
+
function seed(owner) {
|
|
187
|
+
if (seeded[owner]) return;
|
|
188
|
+
seeded[owner] = true;
|
|
189
|
+
var sessions = sm && sm.sessions;
|
|
190
|
+
if (!sessions || typeof sessions.forEach !== "function") return;
|
|
191
|
+
sessions.forEach(function (session) {
|
|
192
|
+
if (sessionOwner(session) !== owner || !session.history) return;
|
|
193
|
+
var vendor = session.vendor || "claude";
|
|
194
|
+
var seenSnapshot = Object.create(null);
|
|
195
|
+
var seenReset = Object.create(null);
|
|
196
|
+
var history = session.history;
|
|
197
|
+
var floor = Math.max(0, history.length - HISTORY_SCAN_LIMIT);
|
|
198
|
+
for (var i = history.length - 1; i >= floor; i--) {
|
|
199
|
+
var item = history[i];
|
|
200
|
+
if (!item || item.type !== "rate_limit_usage" || item.vendor !== vendor) continue;
|
|
201
|
+
var observedAtMs = numberOrNull(item.observedAtMs) !== null ? item.observedAtMs : numberOrNull(item._ts);
|
|
202
|
+
if (item.source === "account_snapshot") {
|
|
203
|
+
if (seenSnapshot[item.limitId]) continue;
|
|
204
|
+
seenSnapshot[item.limitId] = true;
|
|
205
|
+
} else if (item.rateLimitType) {
|
|
206
|
+
if (seenReset[item.rateLimitType]) continue;
|
|
207
|
+
seenReset[item.rateLimitType] = true;
|
|
208
|
+
}
|
|
209
|
+
apply(owner, vendor, session, item, observedAtMs, "history");
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function activeWindows(source, current) {
|
|
215
|
+
var windows = [];
|
|
216
|
+
Object.keys(source.buckets).forEach(function (key) {
|
|
217
|
+
var bucket = source.buckets[key];
|
|
218
|
+
if (bucket.kind === "guard") return;
|
|
219
|
+
if (bucket.resetsAt !== null && bucket.resetsAt <= current) return;
|
|
220
|
+
windows.push({
|
|
221
|
+
kind: bucket.kind, limitId: bucket.limitId || null, key: bucket.key || bucket.rateLimitType || null,
|
|
222
|
+
rateLimitType: bucket.rateLimitType || null, windowDurationMins: bucket.windowDurationMins || null,
|
|
223
|
+
usedPercent: bucket.usedPercent, resetsAt: bucket.resetsAt, status: bucket.status || null,
|
|
224
|
+
observedAtMs: bucket.observedAtMs, origin: bucket.origin,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
return windows;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The newest still-valid source supplies the whole snapshot; sources are
|
|
231
|
+
// never blended.
|
|
232
|
+
function vendorMessage(owner, vendor, installedMap, linuxUser) {
|
|
233
|
+
var entry = owners[owner] && owners[owner][vendor];
|
|
234
|
+
var current = now();
|
|
235
|
+
var candidates = [];
|
|
236
|
+
if (entry) {
|
|
237
|
+
Object.keys(entry.sources).forEach(function (key) {
|
|
238
|
+
var source = entry.sources[key];
|
|
239
|
+
if (!liveSession(owner, vendor, source)) return;
|
|
240
|
+
candidates.push({ source: source, windows: activeWindows(source, current) });
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
candidates.sort(function (a, b) { return compareSources(b.source, a.source); });
|
|
244
|
+
var withWindows = candidates.filter(function (c) { return c.windows.length > 0; });
|
|
245
|
+
var chosen = withWindows[0] || candidates[0] || null;
|
|
246
|
+
return {
|
|
247
|
+
vendor: vendor,
|
|
248
|
+
installed: installedMap ? installedMap[vendor] === true : null,
|
|
249
|
+
windows: chosen ? chosen.windows : [],
|
|
250
|
+
lastObservedAtMs: chosen ? chosen.source.lastObservedAtMs : null,
|
|
251
|
+
sourceSessionId: chosen ? chosen.source.sessionId : null,
|
|
252
|
+
otherSourceCount: Math.max(0, withWindows.length - (withWindows[0] ? 1 : 0)),
|
|
253
|
+
scope: "latest_observing_session",
|
|
254
|
+
capability: capabilities.publicCapability(vendor),
|
|
255
|
+
account: service.peek(owner, linuxUser, vendor),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The requester's own newest live session for a vendor whose query handle can
|
|
260
|
+
// answer an account usage request (Claude). Ownership is re-derived from the
|
|
261
|
+
// session record, never from the socket's claim.
|
|
262
|
+
function accountSession(owner, vendor) {
|
|
263
|
+
if (vendor !== "claude" || !sm || !sm.sessions || typeof sm.sessions.forEach !== "function") return null;
|
|
264
|
+
var found = null;
|
|
265
|
+
sm.sessions.forEach(function (session) {
|
|
266
|
+
if ((session.vendor || "claude") !== vendor || sessionOwner(session) !== owner) return;
|
|
267
|
+
var query = session.queryInstance;
|
|
268
|
+
if (!query || typeof query.getAccountUsage !== "function") return;
|
|
269
|
+
if (!found || (session.lastActivity || 0) >= (found.lastActivity || 0)) found = session;
|
|
270
|
+
});
|
|
271
|
+
return found;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function linuxUserOf(ws) {
|
|
275
|
+
return typeof ctx.getLinuxUserForWs === "function" ? ctx.getLinuxUserForWs(ws) || null : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function overviewMessage(ws, requestId) {
|
|
279
|
+
var owner = socketOwner(ws);
|
|
280
|
+
var vendors = [];
|
|
281
|
+
if (owner) {
|
|
282
|
+
seed(owner);
|
|
283
|
+
var installedMap = null;
|
|
284
|
+
if (typeof ctx.getVendorAvailability === "function") {
|
|
285
|
+
installedMap = Object.create(null);
|
|
286
|
+
(ctx.getVendorAvailability(ws) || []).forEach(function (item) { installedMap[item.id] = item.installed === true; });
|
|
287
|
+
}
|
|
288
|
+
var registry = vendorRegistry.publicInfo();
|
|
289
|
+
var linuxUser = linuxUserOf(ws);
|
|
290
|
+
capabilities.VENDORS.forEach(function (vendor) {
|
|
291
|
+
if (registry[vendor]) vendors.push(vendorMessage(owner, vendor, installedMap, linuxUser));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
var message = {
|
|
295
|
+
type: "vendor_usage_overview", slug: slug, serverNowMs: now(), vendors: vendors,
|
|
296
|
+
userId: ws && ws._clayUser && ws._clayUser.id ? ws._clayUser.id : null,
|
|
297
|
+
};
|
|
298
|
+
if (!owner) message.error = "Not authorized for this project.";
|
|
299
|
+
if (requestId !== undefined) message.requestId = requestId;
|
|
300
|
+
return message;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Recipients are revalidated at send time, not when they connected.
|
|
304
|
+
function publish(owner) {
|
|
305
|
+
clients.forEach(function (ws) {
|
|
306
|
+
if (socketOwner(ws) !== owner) return;
|
|
307
|
+
sendTo(ws, overviewMessage(ws));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function observe(session, message, observedAtMs) {
|
|
312
|
+
var owner = sessionOwner(session);
|
|
313
|
+
if (!owner) return;
|
|
314
|
+
// Seed before the first live observation so older history cannot appear to
|
|
315
|
+
// postdate it, then fold the new one in.
|
|
316
|
+
seed(owner);
|
|
317
|
+
if (apply(owner, (message && message.vendor) || session.vendor || "claude", session, message, observedAtMs, "live")) publish(owner);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleMessage(ws, msg) {
|
|
321
|
+
if (msg.type !== "vendor_usage_get") return false;
|
|
322
|
+
var requestId = typeof msg.requestId === "string" && msg.requestId.length <= 64 ? msg.requestId : undefined;
|
|
323
|
+
if (msg.slug !== undefined && msg.slug !== slug) {
|
|
324
|
+
var wrong = overviewMessage(null, requestId);
|
|
325
|
+
wrong.vendors = [];
|
|
326
|
+
wrong.error = "This request belongs to a different project.";
|
|
327
|
+
sendTo(ws, wrong);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
sendTo(ws, overviewMessage(ws, requestId));
|
|
331
|
+
if (msg.refresh === true) refreshAccounts(ws, requestId, msg);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Starts account reads for installed, query-capable vendors and sends an
|
|
336
|
+
// updated overview, echoing the same requestId, as each one settles. A result
|
|
337
|
+
// is dropped when the socket's live owner or the account changed meanwhile.
|
|
338
|
+
function refreshAccounts(ws, requestId, msg) {
|
|
339
|
+
var owner = socketOwner(ws);
|
|
340
|
+
if (!owner) return;
|
|
341
|
+
var userId = ws._clayUser && ws._clayUser.id ? ws._clayUser.id : null;
|
|
342
|
+
var linuxUser = linuxUserOf(ws);
|
|
343
|
+
var installedMap = null;
|
|
344
|
+
if (typeof ctx.getVendorAvailability === "function") {
|
|
345
|
+
installedMap = Object.create(null);
|
|
346
|
+
(ctx.getVendorAvailability(ws) || []).forEach(function (item) { installedMap[item.id] = item.installed === true; });
|
|
347
|
+
}
|
|
348
|
+
var wanted = typeof msg.vendor === "string" ? [msg.vendor] : capabilities.VENDORS;
|
|
349
|
+
wanted.forEach(function (vendor) {
|
|
350
|
+
if (capabilities.VENDORS.indexOf(vendor) === -1 || capabilities.capabilityFor(vendor).mode !== "query") return;
|
|
351
|
+
if (installedMap && installedMap[vendor] !== true) return;
|
|
352
|
+
var session = accountSession(owner, vendor);
|
|
353
|
+
// Refused and unintegrated vendors are already described by the immediate reply.
|
|
354
|
+
var refused = service.refusal(vendor, linuxUser, !!session);
|
|
355
|
+
if (refused) return;
|
|
356
|
+
function isCurrent() { return socketOwner(ws) === owner && linuxUserOf(ws) === linuxUser && (ws._clayUser && ws._clayUser.id ? ws._clayUser.id : null) === userId; }
|
|
357
|
+
service.read({ owner: owner, linuxUser: linuxUser, vendor: vendor, session: session, force: msg.force === true, isCurrent: isCurrent }).then(function (result) {
|
|
358
|
+
if (result && isCurrent()) sendTo(ws, overviewMessage(ws, requestId));
|
|
359
|
+
}, function () {});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Read-only account usage for a canonical caller that has no socket. The
|
|
364
|
+
// caller supplies the already-authorized owner and OS user plus a liveness
|
|
365
|
+
// check; the same service, identity rules and caches apply. Resolves to the
|
|
366
|
+
// per-vendor entries, or null if the caller stopped being current.
|
|
367
|
+
async function readForOwner(request) {
|
|
368
|
+
var owner = request.owner;
|
|
369
|
+
if (!owner || sessionOwnerKey(owner) === null) return null;
|
|
370
|
+
var isCurrent = typeof request.isCurrent === "function" ? request.isCurrent : function () { return true; };
|
|
371
|
+
var installedMap = request.installed || null;
|
|
372
|
+
var linuxUser = request.linuxUser || null;
|
|
373
|
+
seed(owner);
|
|
374
|
+
var jobs = [];
|
|
375
|
+
capabilities.VENDORS.forEach(function (vendor) {
|
|
376
|
+
if (capabilities.capabilityFor(vendor).mode !== "query") return;
|
|
377
|
+
if (installedMap && installedMap[vendor] !== true) return;
|
|
378
|
+
var session = accountSession(owner, vendor);
|
|
379
|
+
if (service.refusal(vendor, linuxUser, !!session)) return;
|
|
380
|
+
jobs.push(service.read({ owner: owner, linuxUser: linuxUser, vendor: vendor, session: session, force: request.force === true, isCurrent: isCurrent }));
|
|
381
|
+
});
|
|
382
|
+
var results = await Promise.all(jobs.map(function (job) { return job.then(function (value) { return value; }, function () { return undefined; }); }));
|
|
383
|
+
if (!isCurrent() || results.indexOf(null) !== -1) return null;
|
|
384
|
+
var registry = vendorRegistry.publicInfo();
|
|
385
|
+
return capabilities.VENDORS.filter(function (vendor) { return registry[vendor] && (!installedMap || installedMap[vendor] === true); }).map(function (vendor) {
|
|
386
|
+
var entry = vendorMessage(owner, vendor, installedMap, linuxUser);
|
|
387
|
+
return { vendor: vendor, capability: entry.capability, account: entry.account, observed: { windows: entry.windows, lastObservedAtMs: entry.lastObservedAtMs } };
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function sessionOwnerKey(owner) { return multiUser() ? (owner && owner !== DEFAULT_OWNER ? owner : null) : DEFAULT_OWNER; }
|
|
392
|
+
|
|
393
|
+
// Account changes (a vendor login) drop every cached account read.
|
|
394
|
+
function invalidateAll() {
|
|
395
|
+
Object.keys(owners).concat(Object.keys(seeded), [DEFAULT_OWNER]).forEach(function (owner) { service.invalidate(owner); });
|
|
396
|
+
clients.forEach(function (ws) { var owner = socketOwner(ws); if (owner) service.invalidate(owner); });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (sm) {
|
|
400
|
+
quotaSnapshots.attachQuotaObservers(sm);
|
|
401
|
+
sm.addOnQuotaObserved(observe);
|
|
402
|
+
}
|
|
403
|
+
return { handleMessage: handleMessage, observe: observe, overviewMessage: overviewMessage, invalidateAll: invalidateAll, readForOwner: readForOwner, service: service };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = { attachVendorUsageOverview: attachVendorUsageOverview };
|