clay-server 5.2.0-beta.10 → 5.2.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/project-session-notes.js +33 -8
  9. package/lib/project-sessions.js +63 -24
  10. package/lib/project-start-protocol.js +29 -0
  11. package/lib/project-vendor-login.js +2 -0
  12. package/lib/project.js +3 -0
  13. package/lib/public/app.js +27 -18
  14. package/lib/public/css/contextual-clay.css +1 -1
  15. package/lib/public/css/filebrowser.css +1 -1
  16. package/lib/public/css/mate-loading-scene.css +21 -0
  17. package/lib/public/css/overlays.css +64 -256
  18. package/lib/public/css/project-mate-creation.css +5 -6
  19. package/lib/public/css/project-start.css +108 -0
  20. package/lib/public/css/projects-hub.css +53 -14
  21. package/lib/public/css/settings-shell.css +103 -0
  22. package/lib/public/css/sidebar.css +7 -0
  23. package/lib/public/css/title-bar.css +83 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/account-transport-settings.js +8 -2
  26. package/lib/public/modules/admin-settings-email.js +7 -2
  27. package/lib/public/modules/admin-settings-invitations.js +2 -2
  28. package/lib/public/modules/admin-settings-projects.js +2 -2
  29. package/lib/public/modules/admin-settings-ui.js +20 -4
  30. package/lib/public/modules/admin-settings-users.js +4 -4
  31. package/lib/public/modules/app-home-hub.js +2 -1
  32. package/lib/public/modules/app-messages.js +10 -2
  33. package/lib/public/modules/app-projects.js +1 -407
  34. package/lib/public/modules/app-rate-limit.js +12 -1
  35. package/lib/public/modules/clay-tool-presentation.js +1 -0
  36. package/lib/public/modules/contextual-clay.js +3 -1
  37. package/lib/public/modules/github-star.js +66 -0
  38. package/lib/public/modules/home-surface-boot.js +0 -1
  39. package/lib/public/modules/mate-loading-scene.js +17 -0
  40. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  41. package/lib/public/modules/profile.js +11 -3
  42. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  43. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  44. package/lib/public/modules/project-settings-environment.js +12 -1
  45. package/lib/public/modules/project-settings.js +119 -129
  46. package/lib/public/modules/project-start-dialog.js +130 -0
  47. package/lib/public/modules/project-start-folder-state.js +78 -0
  48. package/lib/public/modules/project-start-folder-view.js +190 -0
  49. package/lib/public/modules/project-start-forms.js +66 -0
  50. package/lib/public/modules/project-start-path.js +60 -0
  51. package/lib/public/modules/project-start-state.js +54 -0
  52. package/lib/public/modules/project-start-submit.js +67 -0
  53. package/lib/public/modules/projects-hub-view.js +32 -44
  54. package/lib/public/modules/projects-hub.js +39 -28
  55. package/lib/public/modules/server-settings.js +36 -91
  56. package/lib/public/modules/settings-project-target.js +56 -0
  57. package/lib/public/modules/settings-select.js +1 -1
  58. package/lib/public/modules/settings-shell-registry.js +88 -0
  59. package/lib/public/modules/settings-shell-view.js +104 -0
  60. package/lib/public/modules/settings-shell.js +342 -0
  61. package/lib/public/modules/sidebar-mobile.js +8 -30
  62. package/lib/public/modules/sidebar-projects.js +2 -1
  63. package/lib/public/modules/theme.js +31 -3
  64. package/lib/public/modules/user-settings-integrations.js +2 -1
  65. package/lib/public/modules/user-settings-layout.js +12 -34
  66. package/lib/public/modules/user-settings-profile.js +1 -1
  67. package/lib/public/modules/user-settings.js +18 -85
  68. package/lib/public/modules/vendor-usage-format.js +93 -0
  69. package/lib/public/modules/vendor-usage-list.js +45 -0
  70. package/lib/public/modules/vendor-usage-model.js +215 -0
  71. package/lib/public/modules/vendor-usage-overview.js +323 -0
  72. package/lib/public/style.css +10 -7
  73. package/lib/public/welcome-art/conversation-desk.png +0 -0
  74. package/lib/public/welcome-art/mate-clay.png +0 -0
  75. package/lib/public/welcome-art/project-clay.png +0 -0
  76. package/lib/sdk-bridge.js +6 -1
  77. package/lib/sdk-message-processor.js +9 -1
  78. package/lib/sdk-quota-snapshot.js +20 -2
  79. package/lib/server-github-stars.js +39 -0
  80. package/lib/server-global-ws.js +21 -17
  81. package/lib/server.js +11 -0
  82. package/lib/session-notes-mcp-server.js +26 -4
  83. package/lib/session-notes-writes.js +88 -0
  84. package/lib/session-visibility.js +20 -8
  85. package/lib/sessions.js +2 -0
  86. package/lib/vendor-usage-acp-readers.js +97 -0
  87. package/lib/vendor-usage-capabilities.js +88 -0
  88. package/lib/vendor-usage-http-readers.js +214 -0
  89. package/lib/vendor-usage-metrics.js +47 -0
  90. package/lib/vendor-usage-native-readers.js +106 -0
  91. package/lib/vendor-usage-overview.js +406 -0
  92. package/lib/vendor-usage-service.js +281 -0
  93. package/lib/ws-schema.js +4 -2
  94. package/lib/yoke/adapters/claude.js +12 -0
  95. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  96. package/lib/yoke/adapters/codex.js +45 -12
  97. package/lib/yoke/claude-hook-installer.js +1 -0
  98. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  99. package/lib/yoke/codex-model-catalog.js +37 -3
  100. package/lib/yoke/muse-services.js +16 -3
  101. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  102. package/lib/yoke/vendor-registry.js +6 -2
  103. package/package.json +2 -1
  104. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -0,0 +1,281 @@
1
+ // Shared acquisition service for vendor account usage.
2
+ //
3
+ // One place decides whether a direct read is allowed, runs it with a bounded
4
+ // timeout (cancelling the underlying work), shares an in-flight read between
5
+ // callers, caches results and backs off after failures. Provider-specific
6
+ // parsing lives in the reader modules; the capability table says which vendors
7
+ // have a reader at all.
8
+ //
9
+ // Identity: a direct read is allowed only when the credential provably belongs
10
+ // to the requester. In account mode, vendors that run as the server's own user
11
+ // (Muse, Kimi, Grok, Kiro) and Codex without a mapped OS user are refused with a
12
+ // clear status instead of showing the server user's account to every owner.
13
+ // Cache entries are keyed by owner + OS user + vendor. Where a cheap credential
14
+ // fingerprint exists (Kimi, Grok) it is re-read before cached figures are
15
+ // returned (including while a read is in flight) and again when a read
16
+ // completes; rotation or deletion discards the figures. Figures from a source
17
+ // whose account cannot be verified (Muse, Kiro, Claude, Codex) are shown for at
18
+ // most one TTL (60 s) from the reading: after that, in peek and read alike,
19
+ // they are replaced by an explicit "expired" state with no numbers, and a failed
20
+ // refresh never extends them. Verified-identity figures may be kept, marked
21
+ // stale, across a transient failure for at most `staleMaxMs`. A Claude reading
22
+ // is bound to the exact live session and query that produced it and is dropped
23
+ // when that source is replaced. Sign-in failures and account changes always drop
24
+ // figures. A result is used only if the caller is still current when the work
25
+ // starts, while it waits, and when it settles.
26
+
27
+ var capabilities = require("./vendor-usage-capabilities");
28
+ var metrics = require("./vendor-usage-metrics");
29
+ var nativeReaders = require("./vendor-usage-native-readers");
30
+ var httpReaders = require("./vendor-usage-http-readers");
31
+ var acpReaders = require("./vendor-usage-acp-readers");
32
+
33
+ var DEFAULTS = { ttlMs: 60 * 1000, minForceMs: 10 * 1000, timeoutMs: 15 * 1000, backoffBaseMs: 30 * 1000, backoffMaxMs: 10 * 60 * 1000, noAuthRetryMs: 5 * 60 * 1000, staleMaxMs: 10 * 60 * 1000 };
34
+
35
+ // Failures that mean the credential or account is not (or no longer) the one
36
+ // the cached figures described, so those figures must not be shown again.
37
+ var DROPS_FIGURES = ["no_auth", "expired_auth", "unauthorized", "untrusted_origin", "account_changed", "source_changed"];
38
+
39
+ var MESSAGES = {
40
+ identity_unprovable: "Clay cannot prove which account this vendor uses for you, so it will not read one.",
41
+ account_changed: "The saved sign-in changed while usage was being read; figures from the previous account were discarded.",
42
+ source_changed: "The session that reported this usage was replaced; its figures were discarded.",
43
+ expired: "The last reading is too old and its account cannot be verified, so it is not shown. Read again.",
44
+ };
45
+
46
+ function defaultFetchJson(timeoutMs) {
47
+ return async function (url, headers) {
48
+ var response = await fetch(url, { headers: headers, signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
49
+ var body = null;
50
+ try { body = await response.json(); } catch (e) { body = null; }
51
+ return { status: response.status, body: body };
52
+ };
53
+ }
54
+
55
+ function createUsageService(options) {
56
+ options = options || {};
57
+ var now = typeof options.now === "function" ? options.now : Date.now;
58
+ var timing = Object.assign({}, DEFAULTS, options.timing || {});
59
+ var fs = require("fs");
60
+ var os = require("os");
61
+ var httpDeps = Object.assign({
62
+ readText: function (file) { return fs.promises.readFile(file, "utf8"); },
63
+ readTextSync: function (file) { return fs.readFileSync(file, "utf8"); },
64
+ fetchJson: defaultFetchJson(timing.timeoutMs),
65
+ env: process.env, homedir: os.homedir,
66
+ }, options.http || {});
67
+ httpDeps.now = now;
68
+ var acpDeps = Object.assign({
69
+ binary: function () { try { return require("./yoke/kiro-acp-server").findKiroPath(); } catch (e) { return null; } },
70
+ spawn: function (command, args, spawnOptions) { return require("child_process").spawn(command, args, spawnOptions); },
71
+ tmpdir: os.tmpdir, env: process.env,
72
+ }, options.acp || {});
73
+ var readers = Object.assign({
74
+ claude: nativeReaders.claude,
75
+ codex: nativeReaders.codex,
76
+ muse: nativeReaders.muse,
77
+ kimi: function () { return httpReaders.kimi(httpDeps); },
78
+ grok: function () { return httpReaders.grok(httpDeps); },
79
+ kiro: function (context) { return acpReaders.kiro(context, acpDeps); },
80
+ }, options.readers || {});
81
+ var identities = Object.assign({
82
+ kimi: function () { return httpReaders.identities.kimi(httpDeps); },
83
+ grok: function () { return httpReaders.identities.grok(httpDeps); },
84
+ }, options.identities || {});
85
+ var multiUser = typeof options.multiUser === "function" ? options.multiUser : function () { return false; };
86
+ var osUsersEnabled = typeof options.osUsersEnabled === "function" ? options.osUsersEnabled : function () { return false; };
87
+ var sourceLive = typeof options.sourceLive === "function" ? options.sourceLive : function () { return true; };
88
+ var getAdapter = typeof options.getAdapter === "function" ? options.getAdapter : function () { return null; };
89
+ var entries = Object.create(null);
90
+ var generations = Object.create(null);
91
+
92
+ function keyOf(owner, linuxUser, vendor) { return owner + "\u0000" + (linuxUser || "") + "\u0000" + vendor; }
93
+
94
+ function identityOf(vendor) {
95
+ try { return identities[vendor] ? identities[vendor]() : null; } catch (e) { return null; }
96
+ }
97
+
98
+ function refusal(vendor, linuxUser, hasSession) {
99
+ var cap = capabilities.capabilityFor(vendor);
100
+ if (cap.mode !== "query") return { status: "unintegrated", failureKind: "unintegrated", message: cap.reason || "No usage source is integrated." };
101
+ if (cap.identity === "owner-session") return hasSession === false ? { status: "idle", failureKind: null, message: null } : null;
102
+ if (!multiUser()) return null;
103
+ if (cap.identity === "owner-os-user" && osUsersEnabled() && linuxUser) return null;
104
+ return { status: "refused", failureKind: "identity_unprovable", message: MESSAGES.identity_unprovable };
105
+ }
106
+
107
+ function blank(status, failureKind, message) {
108
+ return { status: status, metrics: [], fetchedAtMs: null, stale: false, failureKind: failureKind || null, message: message || null, retryAtMs: null, planType: null, accountId: null, fetching: false };
109
+ }
110
+
111
+ function bindingOf(session) {
112
+ return session ? { session: session, query: session.queryInstance } : null;
113
+ }
114
+
115
+ function bindingLive(binding) {
116
+ return !binding || (binding.session.queryInstance === binding.query && sourceLive(binding.session) !== false);
117
+ }
118
+
119
+ function sameBinding(a, b) {
120
+ return (!a && !b) || (!!a && !!b && a.session === b.session && a.query === b.query);
121
+ }
122
+
123
+ // Figures whose account cannot be verified live only for one TTL; verified
124
+ // ones may stay (stale) a bounded while. Past that the numbers are removed.
125
+ function expire(entry) {
126
+ var a = entry.account;
127
+ if (!a.metrics.length) return;
128
+ var age = a.fetchedAtMs === null ? Infinity : now() - a.fetchedAtMs;
129
+ var limit = entry.identity ? timing.staleMaxMs : timing.ttlMs;
130
+ if (age < limit) return;
131
+ entry.account = { status: "idle", metrics: [], fetchedAtMs: null, stale: false, failureKind: "expired", message: MESSAGES.expired, planType: null, accountId: null };
132
+ entry.identity = null;
133
+ }
134
+
135
+ function snapshotOf(entry) {
136
+ expire(entry);
137
+ var a = entry.account;
138
+ return {
139
+ status: a.status, metrics: a.metrics.slice(), fetchedAtMs: a.fetchedAtMs, stale: a.stale === true,
140
+ failureKind: a.failureKind || null, message: a.message || null, retryAtMs: entry.retryAtMs || null,
141
+ planType: a.planType || null, accountId: a.accountId || null, sourceSessionId: a.sourceSessionId === undefined ? null : a.sourceSessionId,
142
+ fetching: !!entry.inflight,
143
+ };
144
+ }
145
+
146
+ function emptyEntry() {
147
+ return { account: { status: "idle", metrics: [], fetchedAtMs: null }, identity: null, binding: null, failures: 0, retryAtMs: 0, inflight: null, token: 0 };
148
+ }
149
+
150
+ function drop(key) {
151
+ var entry = entries[key];
152
+ if (!entry) return;
153
+ entry.token++;
154
+ delete entries[key];
155
+ }
156
+
157
+ // True when the cached projection no longer describes the live source: the
158
+ // credential fingerprint differs (rotation or deletion, even mid-read) or the
159
+ // session/query that produced it was replaced.
160
+ function sourceChanged(entry, vendor) {
161
+ if (entry.identity && identityOf(vendor) !== entry.identity) return true;
162
+ return !bindingLive(entry.binding);
163
+ }
164
+
165
+ // The current known state, without starting a read.
166
+ function peek(owner, linuxUser, vendor) {
167
+ var refused = refusal(vendor, linuxUser, null);
168
+ if (refused && refused.status !== "idle") return blank(refused.status, refused.failureKind, refused.message);
169
+ var key = keyOf(owner, linuxUser, vendor);
170
+ var entry = entries[key];
171
+ if (!entry) return blank("idle");
172
+ if (sourceChanged(entry, vendor)) { drop(key); return blank("idle"); }
173
+ return snapshotOf(entry);
174
+ }
175
+
176
+ function failureDelay(entry, kind) {
177
+ if (kind === "no_auth") return timing.noAuthRetryMs;
178
+ return Math.min(timing.backoffMaxMs, timing.backoffBaseMs * Math.pow(2, Math.max(0, entry.failures - 1)));
179
+ }
180
+
181
+ function settleFailure(entry, error) {
182
+ var kind = error && error.failureKind ? error.failureKind : "error";
183
+ entry.failures += 1;
184
+ entry.retryAtMs = now() + failureDelay(entry, kind);
185
+ var prior = entry.account;
186
+ // Only a transient failure keeps the last good figures (marked stale);
187
+ // a sign-in problem or account change must not leave a former account's numbers.
188
+ // Figures survive only when their account is verifiable (a fingerprint) and
189
+ // they are still inside the bounded stale window.
190
+ var keep = DROPS_FIGURES.indexOf(kind) === -1 && !!entry.identity && prior.metrics.length > 0 && (prior.status === "ok" || prior.stale === true) && prior.fetchedAtMs !== null && now() - prior.fetchedAtMs < timing.staleMaxMs;
191
+ entry.account = {
192
+ status: kind === "no_auth" ? "no_auth" : kind === "expired_auth" ? "expired_auth" : "error",
193
+ metrics: keep ? prior.metrics : [], fetchedAtMs: keep ? prior.fetchedAtMs : null, stale: keep,
194
+ failureKind: kind, message: error && error.failureKind ? error.message : "Usage could not be read.",
195
+ planType: keep ? prior.planType || null : null, accountId: keep ? prior.accountId || null : null,
196
+ sourceSessionId: keep ? prior.sourceSessionId : null,
197
+ };
198
+ if (!keep) { entry.identity = null; entry.binding = null; }
199
+ }
200
+
201
+ function withTimeout(promise, controller) {
202
+ var timer;
203
+ var limit = new Promise(function (resolve, reject) {
204
+ timer = setTimeout(function () {
205
+ controller.abort();
206
+ reject(nativeReaders.failure("timeout", "The usage read timed out."));
207
+ }, timing.timeoutMs);
208
+ });
209
+ return Promise.race([promise, limit]).then(function (v) { clearTimeout(timer); return v; }, function (e) { clearTimeout(timer); throw e; });
210
+ }
211
+
212
+ // Resolves to a snapshot, or null when the result must not be used because
213
+ // the caller or the account changed. Nothing is started for a caller that is
214
+ // already stale.
215
+ async function read(request) {
216
+ var owner = request.owner, linuxUser = request.linuxUser || null, vendor = request.vendor;
217
+ var isCurrent = typeof request.isCurrent === "function" ? request.isCurrent : function () { return true; };
218
+ if (!isCurrent()) return null;
219
+ var refused = refusal(vendor, linuxUser, !!request.session);
220
+ if (refused) return refused.status === "idle" ? peek(owner, linuxUser, vendor) : blank(refused.status, refused.failureKind, refused.message);
221
+ var key = keyOf(owner, linuxUser, vendor);
222
+ var entry = entries[key] || (entries[key] = emptyEntry());
223
+ var startedGeneration = generations[owner] || 0;
224
+ var before = identityOf(vendor);
225
+ var binding = bindingOf(request.session);
226
+ if ((entry.identity && before !== entry.identity) || !bindingLive(entry.binding) || (entry.binding && !sameBinding(entry.binding, binding))) {
227
+ drop(key);
228
+ entry = entries[key] = emptyEntry();
229
+ }
230
+ expire(entry);
231
+ var current = now();
232
+ var age = entry.account.fetchedAtMs === null ? Infinity : current - entry.account.fetchedAtMs;
233
+ var coolingDown = entry.retryAtMs > current;
234
+ var fresh = entry.account.status === "ok" && age < (request.force ? timing.minForceMs : timing.ttlMs);
235
+ if (!entry.inflight && (fresh || coolingDown)) return isCurrent() ? snapshotOf(entry) : null;
236
+ if (!entry.inflight) {
237
+ var token = ++entry.token;
238
+ var controller = new AbortController();
239
+ var context = { adapter: getAdapter(vendor), linuxUser: linuxUser, session: request.session || null, signal: controller.signal };
240
+ entry.binding = binding;
241
+ var run = Promise.resolve().then(function () { return withTimeout(readers[vendor](context), controller); });
242
+ entry.inflight = run.then(function (result) {
243
+ if (entry.token !== token || (generations[owner] || 0) !== startedGeneration) return;
244
+ var list = (result && Array.isArray(result.metrics) ? result.metrics : []).filter(metrics.valid);
245
+ if (list.length === 0) throw nativeReaders.failure("unavailable", "The vendor reported no usage figures for this account.");
246
+ if (!bindingLive(binding)) throw nativeReaders.failure("source_changed", MESSAGES.source_changed);
247
+ // The credential is re-read AFTER the response. For a fingerprinted
248
+ // source it must equal the one the reader used (or, failing that, the
249
+ // one seen before the call); rotation, deletion or first appearance
250
+ // mid-read means neither account's figures can be attributed.
251
+ var expected = result.identity || before;
252
+ if (identities[vendor]) {
253
+ var fresh = identityOf(vendor);
254
+ if (fresh !== expected) throw nativeReaders.failure("account_changed", MESSAGES.account_changed);
255
+ }
256
+ entry.failures = 0; entry.retryAtMs = 0;
257
+ entry.identity = identities[vendor] ? expected || null : null;
258
+ entry.account = { status: "ok", metrics: list, fetchedAtMs: Number.isFinite(result.observedAtMs) ? result.observedAtMs : now(), stale: false, planType: result.planType || null, accountId: result.accountId || null, sourceSessionId: result.sourceSessionId === undefined ? null : result.sourceSessionId };
259
+ }).catch(function (error) {
260
+ if (entry.token !== token || (generations[owner] || 0) !== startedGeneration) return;
261
+ settleFailure(entry, error);
262
+ }).then(function () { if (entry.token === token) entry.inflight = null; });
263
+ }
264
+ await entry.inflight;
265
+ if (!isCurrent() || (generations[owner] || 0) !== startedGeneration || entries[key] !== entry) return null;
266
+ return snapshotOf(entry);
267
+ }
268
+
269
+ // Forget everything for an owner (account switch, logout, access change);
270
+ // reads still in flight can no longer store or return a result.
271
+ function invalidate(owner) {
272
+ generations[owner] = (generations[owner] || 0) + 1;
273
+ Object.keys(entries).forEach(function (key) {
274
+ if (key.indexOf(owner + "\u0000") === 0) drop(key);
275
+ });
276
+ }
277
+
278
+ return { read: read, peek: peek, invalidate: invalidate, refusal: refusal };
279
+ }
280
+
281
+ module.exports = { createUsageService: createUsageService, DEFAULTS: DEFAULTS };
package/lib/ws-schema.js CHANGED
@@ -37,6 +37,8 @@ var schema = {
37
37
  "session_folders_state": { direction: "s2c", handler: "lib/public/modules/session-folders.js", description: "Authoritative per-user, per-project session folder state; sent on connect and after every change to all of that user's sockets" },
38
38
  "new_session_options_get": { direction: "c2s", handler: "lib/project-session-creation.js", description: "Ask for the installed providers, the project's default provider and whether the caller may set it" },
39
39
  "new_session_default_set": { direction: "c2s", handler: "lib/project-session-creation.js", description: "Save the project-scoped default provider the inline New session row preselects" },
40
+ "vendor_usage_get": { direction: "c2s", handler: "lib/vendor-usage-overview.js", description: "Ask for the caller's observed per-vendor usage overview for this project" },
41
+ "vendor_usage_overview": { direction: "s2c", handler: "lib/public/modules/vendor-usage-overview.js", description: "Owner-scoped observed vendor usage windows, reset times and observation times; unknown vendors carry no windows" },
40
42
  "new_session_options": { direction: "s2c", handler: "lib/public/modules/session-create.js", description: "Installed providers, project default provider and default-setting permission for the inline New session row" },
41
43
  "new_session_default_result": { direction: "s2c", handler: "lib/public/modules/session-create.js", description: "Outcome of saving the project New session default" },
42
44
  "new_session_project_default": { direction: "s2c", handler: "lib/public/modules/session-create.js", description: "The project New session default changed" },
@@ -404,9 +406,9 @@ var schema = {
404
406
  // -----------------------------------------------------------------------
405
407
  // Projects
406
408
  // -----------------------------------------------------------------------
407
- "browse_dir": { direction: "c2s", handler: "lib/project-sessions.js", description: "Browse directory entries for project creation" },
409
+ "browse_dir": { direction: "c2s", handler: "lib/project-sessions.js", description: "Browse directory entries for project creation (requestId echoed; reply adds canonical dir, prefix, permitted parent)" },
408
410
  "browse_dir_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Directory listing result" },
409
- "add_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Add an existing directory as a project" },
411
+ "add_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Add an existing directory as a project (optional requestId echoed on add_project_result, also for create/clone)" },
410
412
  "add_project_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Add project result" },
411
413
  "create_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new empty project directory" },
412
414
  "clone_project": { direction: "c2s", handler: "lib/project-sessions.js", description: "Clone a git repository as a new project" },
@@ -704,6 +704,15 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
704
704
  return Promise.resolve(null);
705
705
  },
706
706
 
707
+ // Account plan usage through the SDK's usage call, which the SDK marks
708
+ // experimental: guarded by capability, resolves null when absent, and the
709
+ // caller validates the shape. Transcript scanning is skipped.
710
+ getAccountUsage: function() {
711
+ var method = rawQuery && rawQuery.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
712
+ if (typeof method === "function") return Promise.resolve(method.call(rawQuery, { skipBehaviors: true }));
713
+ return Promise.resolve(null);
714
+ },
715
+
707
716
  supportedModels: function() {
708
717
  if (rawQuery && typeof rawQuery.supportedModels === "function") {
709
718
  return rawQuery.supportedModels();
@@ -1482,6 +1491,9 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1482
1491
  return iterEnded ? Promise.resolve(null) : contextRequests.request(options);
1483
1492
  },
1484
1493
 
1494
+ // The OS-isolated worker protocol has no account usage request.
1495
+ getAccountUsage: function() { return Promise.resolve(null); },
1496
+
1485
1497
  supportedModels: function() {
1486
1498
  return Promise.resolve([]);
1487
1499
  },
@@ -7,6 +7,7 @@ var fields = {
7
7
  issues: "codexIssuesToolCatalogVersion",
8
8
  knowledge: "codexYouToolCatalogVersion",
9
9
  linear: "codexLinearToolCatalogVersion",
10
+ notes: "codexNotesToolCatalogVersion",
10
11
  };
11
12
  module.exports = {
12
13
  toolTransport: "dynamic",
@@ -17,6 +17,7 @@ var createTurnStop = require("../codex-turn-stop").createTurnStop;
17
17
  var idleRecovery = require("../codex-idle-recovery");
18
18
  var modelCatalog = require("../codex-model-catalog");
19
19
  var normalizeQuotaSnapshot = require("../codex-rate-limits").normalizeSnapshot;
20
+ var dynamicToolNames = require("../codex-dynamic-tool-names");
20
21
 
21
22
  // --- Reasoning defaults ---
22
23
  // Reasoning summaries are the only readable thinking channel Codex offers:
@@ -33,7 +34,7 @@ var normalizeQuotaSnapshot = require("../codex-rate-limits").normalizeSnapshot;
33
34
  var CODEX_REASONING_DEFAULTS = {
34
35
  model_reasoning_summary: "detailed",
35
36
  };
36
- var DEFAULT_CODEX_MODEL = "gpt-6-astra";
37
+ var DEFAULT_CODEX_MODEL = modelCatalog.DEFAULT_MODEL;
37
38
  var CODEX_QUERY_BOUND_TOOL_CAPABILITY = { mode: "dynamic", resume: "session-mcp-recovery" };
38
39
  var CODEX_NOTE_TOOLS = {
39
40
  list_notes: true,
@@ -622,6 +623,8 @@ function flattenEvent(notification, state) {
622
623
  // Dynamic tools are supplied by Clay for a specific thread, such as the
623
624
  // Driver's pair-session controls.
624
625
  if (item.type === "dynamicToolCall" || item.type === "dynamic_tool_call") {
626
+ // Activity shows the original Clay name, not the Codex wire alias.
627
+ var dynamicDisplayName = state.dynamicToolCatalog ? state.dynamicToolCatalog.displayName(item.tool) : item.tool;
625
628
  if (!state.toolBlocks[item.id]) {
626
629
  state.blockCounter++;
627
630
  state.toolBlocks[item.id] = "blk_" + state.blockCounter;
@@ -630,13 +633,13 @@ function flattenEvent(notification, state) {
630
633
  yokeType: "tool_start",
631
634
  blockId: dynamicBlockId,
632
635
  toolId: item.id,
633
- toolName: item.tool || "session_tool",
636
+ toolName: dynamicDisplayName || "session_tool",
634
637
  });
635
638
  events.push({
636
639
  yokeType: "tool_executing",
637
640
  blockId: dynamicBlockId,
638
641
  toolId: item.id,
639
- toolName: item.tool || "session_tool",
642
+ toolName: dynamicDisplayName || "session_tool",
640
643
  input: item.arguments || {},
641
644
  });
642
645
  }
@@ -968,6 +971,15 @@ function createCodexQueryHandle(appServer, queryOpts) {
968
971
  // server request and waits for the client to execute and answer it.
969
972
  if (method === "item/tool/call") {
970
973
  var dynamicParams = msg.params || {};
974
+ var resolvedDynamicTool = (state.dynamicToolCatalog || dynamicToolNames.createCatalog([])).resolve(dynamicParams.tool, dynamicParams.namespace);
975
+ if (resolvedDynamicTool.error) {
976
+ appServer.respond(msg.id, {
977
+ contentItems: [{ type: "inputText", text: "Error: " + resolvedDynamicTool.error }],
978
+ success: false,
979
+ });
980
+ return;
981
+ }
982
+ var dynamicToolName = resolvedDynamicTool.name;
971
983
  if (!callDynamicTool) {
972
984
  appServer.respond(msg.id, {
973
985
  contentItems: [{ type: "inputText", text: "Session tool handler is unavailable" }],
@@ -976,7 +988,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
976
988
  return;
977
989
  }
978
990
  function executeDynamicTool() {
979
- return Promise.resolve(callDynamicTool(dynamicParams.tool, dynamicParams.arguments || {})).then(function (result) {
991
+ return Promise.resolve(callDynamicTool(dynamicToolName, dynamicParams.arguments || {})).then(function (result) {
980
992
  var content = result && Array.isArray(result.content) ? result.content : [];
981
993
  var contentItems = content.map(function (item) {
982
994
  return { type: "inputText", text: item && item.text ? item.text : JSON.stringify(item) };
@@ -987,7 +999,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
987
999
  appServer.respond(msg.id, { contentItems: contentItems, success: !(result && result.isError) });
988
1000
  });
989
1001
  }
990
- var permissionToolName = canonicalDynamicPermissionToolName(dynamicParams.tool);
1002
+ var permissionToolName = canonicalDynamicPermissionToolName(dynamicToolName);
991
1003
  var permission = canUseTool
992
1004
  ? Promise.resolve(canUseTool(permissionToolName, dynamicParams.arguments || {}, {
993
1005
  toolUseID: dynamicParams.callId || String(msg.id),
@@ -1158,13 +1170,15 @@ function createCodexQueryHandle(appServer, queryOpts) {
1158
1170
  if (queryOpts.webSearchMode) {
1159
1171
  threadParams.webSearchMode = queryOpts.webSearchMode;
1160
1172
  }
1161
- if (Array.isArray(queryOpts.dynamicTools) && queryOpts.dynamicTools.length > 0) {
1162
- threadParams.dynamicTools = queryOpts.dynamicTools.map(function (tool) {
1173
+ // Built on resume too: the persisted catalog uses the same aliases.
1174
+ state.dynamicToolCatalog = dynamicToolNames.createCatalog(Array.isArray(queryOpts.dynamicTools) ? queryOpts.dynamicTools : []);
1175
+ if (state.dynamicToolCatalog.advertised.length > 0) {
1176
+ threadParams.dynamicTools = state.dynamicToolCatalog.advertised.map(function (entry) {
1163
1177
  return {
1164
1178
  type: "function",
1165
- name: tool.name,
1166
- description: tool.description || tool.name,
1167
- inputSchema: dynamicInputSchema(tool.inputSchema),
1179
+ name: entry.name,
1180
+ description: entry.def.description || entry.def.name,
1181
+ inputSchema: dynamicInputSchema(entry.def.inputSchema),
1168
1182
  };
1169
1183
  });
1170
1184
  }
@@ -1558,7 +1572,7 @@ function createCodexAdapter(opts) {
1558
1572
  function buildReadyResponse(skillNames) {
1559
1573
  return {
1560
1574
  models: _cachedModels,
1561
- defaultModel: DEFAULT_CODEX_MODEL,
1575
+ defaultModel: modelCatalog.defaultModel(_cachedModels),
1562
1576
  skills: skillNames || [],
1563
1577
  slashCommands: skillNames || [],
1564
1578
  fastModeState: null,
@@ -1943,7 +1957,7 @@ function createCodexAdapter(opts) {
1943
1957
  throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
1944
1958
  }
1945
1959
 
1946
- var model = queryOpts.model || DEFAULT_CODEX_MODEL;
1960
+ var model = queryOpts.model || modelCatalog.defaultModel(_cachedModels);
1947
1961
  var ac = queryOpts.abortController || new AbortController();
1948
1962
  var activeEntry = {
1949
1963
  abort: function() {
@@ -2036,6 +2050,25 @@ function createCodexAdapter(opts) {
2036
2050
  return { handle: handle, backendPersistence: "ephemeral" };
2037
2051
  },
2038
2052
 
2053
+ // Reads the account's own quota through the already-running app-server;
2054
+ // it never starts a runtime and never starts a turn. Routed to the mapped
2055
+ // OS user's runtime exactly like the other per-owner operations.
2056
+ readAccountRateLimits: async function(opts) {
2057
+ var requestedLinuxUser = opts && opts.linuxUser;
2058
+ if (requestedLinuxUser) {
2059
+ // Throws on a runtime bound to a different OS user; never answers from another identity.
2060
+ var userRuntime = getUserRuntime(requestedLinuxUser);
2061
+ if (userRuntime !== adapter) return userRuntime.readAccountRateLimits(withoutLinuxUser(opts));
2062
+ }
2063
+ if (_requiresLinuxUser) throw new Error("Codex requires a mapped Linux user while OS-user isolation is enabled");
2064
+ if (!_appServer || !_appServer.started) {
2065
+ var idle = new Error("Codex has not started in this project yet; open a Codex session to read account usage.");
2066
+ idle.failureKind = "runtime_idle";
2067
+ throw idle;
2068
+ }
2069
+ return _appServer.send("account/rateLimits/read", { excludeResetCreditDetails: true }, 15000);
2070
+ },
2071
+
2039
2072
  // --- Title generation ---
2040
2073
  generateTitle: async function(messages, opts) {
2041
2074
  var requestedLinuxUser = opts && opts.linuxUser;
@@ -67,6 +67,7 @@ var CLAY_MANAGED_ALLOW = [
67
67
  "mcp__clay-workspace__read_workspace_receipt",
68
68
  "mcp__clay-workspace__inspect_workspace_change",
69
69
  "mcp__clay-workspace__control_project_workspace",
70
+ "mcp__clay-workspace__read_vendor_usage",
70
71
  "mcp__clay-workspace__read_project_records",
71
72
  "mcp__clay-workspace__inspect_project_record_change",
72
73
  "mcp__clay-workspace__control_project_records",
@@ -0,0 +1,80 @@
1
+ // Reversible Codex transport names for Clay dynamic tools.
2
+ //
3
+ // Codex app-server (rust-v0.162.1 validate_dynamic_tools) rejects a
4
+ // thread/start whose dynamic tool names do not match ^[a-zA-Z0-9_-]+$, exceed
5
+ // 128 characters, repeat, or are "mcp" / start with "mcp__". Clay keeps its
6
+ // original names (for example mcp__server__tool) for dispatch, permissions and
7
+ // activity, and only the Codex wire uses a deterministic alias. Determinism
8
+ // lets a resumed thread's persisted catalog resolve against a rebuilt catalog.
9
+
10
+ var crypto = require("crypto");
11
+
12
+ var MAX_NAME_LENGTH = 128;
13
+ var ALIAS_PREFIX = "clay__";
14
+ var HASH_LENGTH = 16;
15
+ var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
16
+
17
+ function isReserved(name) {
18
+ return name === "mcp" || name.indexOf("mcp__") === 0;
19
+ }
20
+
21
+ // True when Codex accepts the name as-is and it cannot be mistaken for an alias.
22
+ function isPlainName(name) {
23
+ return typeof name === "string" && name.length > 0 && name.length <= MAX_NAME_LENGTH
24
+ && IDENTIFIER.test(name) && !isReserved(name) && name.indexOf(ALIAS_PREFIX) !== 0;
25
+ }
26
+
27
+ function transportName(name) {
28
+ if (typeof name !== "string" || !name) throw new Error("Dynamic tool names must be non-empty strings.");
29
+ if (isPlainName(name)) return name;
30
+ var hash = crypto.createHash("sha256").update(name, "utf8").digest("hex").slice(0, HASH_LENGTH);
31
+ var readable = name.replace(/^mcp__/, "").replace(/[^A-Za-z0-9_-]/g, "_");
32
+ var room = MAX_NAME_LENGTH - ALIAS_PREFIX.length - 2 - HASH_LENGTH;
33
+ return ALIAS_PREFIX + readable.slice(0, room) + "__" + hash;
34
+ }
35
+
36
+ // Build the exact alias map for one query's catalog. Repeated identical
37
+ // originals resolve to the same handler, so they are advertised once. Two
38
+ // different originals sharing an alias fail closed instead of guessing.
39
+ function createCatalog(defs) {
40
+ var byTransport = new Map();
41
+ var byOriginal = new Map();
42
+ var advertised = [];
43
+ (defs || []).forEach(function (def) {
44
+ var original = def && def.name;
45
+ var alias = transportName(original);
46
+ if (byOriginal.has(original)) return;
47
+ if (byTransport.has(alias)) {
48
+ throw new Error("Codex dynamic tool names collide: " + byTransport.get(alias) + " and " + original);
49
+ }
50
+ byTransport.set(alias, original);
51
+ byOriginal.set(original, alias);
52
+ advertised.push({ def: def, name: alias });
53
+ });
54
+
55
+ // Map a Codex call back to the original Clay name. Plain names keep the
56
+ // existing live-catalog lookup; aliases must belong to this catalog.
57
+ function resolve(tool, namespace) {
58
+ if (namespace !== undefined && namespace !== null && namespace !== "") {
59
+ return { error: "Clay does not register namespaced dynamic tools: " + namespace };
60
+ }
61
+ if (typeof tool !== "string" || !tool) return { error: "Dynamic tool call has no tool name." };
62
+ if (byTransport.has(tool)) return { name: byTransport.get(tool) };
63
+ if (isPlainName(tool)) return { name: tool };
64
+ return { error: "Session tool is not in this conversation's current catalog: " + tool };
65
+ }
66
+
67
+ function displayName(tool) {
68
+ return byTransport.has(tool) ? byTransport.get(tool) : tool;
69
+ }
70
+
71
+ return { advertised: advertised, resolve: resolve, displayName: displayName };
72
+ }
73
+
74
+ module.exports = {
75
+ MAX_NAME_LENGTH: MAX_NAME_LENGTH,
76
+ ALIAS_PREFIX: ALIAS_PREFIX,
77
+ isPlainName: isPlainName,
78
+ transportName: transportName,
79
+ createCatalog: createCatalog,
80
+ };
@@ -1,5 +1,16 @@
1
1
  // Runtime catalogs are scoped to the adapter's OS user and provider configuration.
2
- var FALLBACK = ["gpt-6-astra", "gpt-6.1-sol", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.2"];
2
+ // The runtime model/list is authoritative. This offline fallback mirrors the
3
+ // picker-visible presets bundled with @openai/codex 0.162.1, in their priority
4
+ // order, minus models the official Codex models page lists as retired for
5
+ // ChatGPT sign-in or deprecated (gpt-5.5, gpt-5.2). Verified 2026-10-17:
6
+ // https://learn.chatgpt.com/docs/models
7
+ var FALLBACK = ["gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
8
+ // Codex marks its highest-priority picker-visible preset as default; for the
9
+ // bundled 0.162.1 presets that is gpt-6.1-sol. Used only without a runtime flag.
10
+ var DEFAULT_MODEL = "gpt-6.1-sol";
11
+ // Fast, low-cost models for title generation, newest first (gpt-5.6-luna's
12
+ // bundled upgrade target is gpt-6-luna).
13
+ var TITLE_MODELS = ["gpt-6-luna", "gpt-5.6-luna"];
3
14
 
4
15
  function fallback() {
5
16
  return FALLBACK.slice();
@@ -21,6 +32,8 @@ async function load(server) {
21
32
  if (!entry || entry.hidden || typeof entry.model !== "string" || !entry.model || seen.has(entry.model)) return;
22
33
  seen.add(entry.model);
23
34
  var model = { value: entry.model, displayName: entry.displayName || entry.model, description: entry.description || "" };
35
+ if (typeof entry.defaultReasoningEffort === "string" && entry.defaultReasoningEffort) model.defaultReasoningEffort = entry.defaultReasoningEffort;
36
+ if (entry.isDefault === true) model.isDefault = true;
24
37
  if (Array.isArray(entry.supportedReasoningEfforts)) {
25
38
  model.supportedEffortLevels = entry.supportedReasoningEfforts.map(function (option) { return option.reasoningEffort; }).filter(function (effort) { return typeof effort === "string" && effort.length > 0; });
26
39
  }
@@ -37,11 +50,32 @@ async function load(server) {
37
50
  return fallback();
38
51
  }
39
52
 
53
+ // The runtime's own default wins. Without a flag, never name a model the
54
+ // catalog does not offer: use the static default only when it is listed,
55
+ // otherwise the first valid row. An empty catalog keeps the static default.
56
+ function defaultModel(models) {
57
+ var values = [];
58
+ var flagged = null;
59
+ (models || []).forEach(function (entry) {
60
+ var value = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.value : null;
61
+ if (typeof value !== "string" || !value.trim()) return;
62
+ values.push(value);
63
+ if (!flagged && typeof entry === "object" && entry.isDefault === true) flagged = value;
64
+ });
65
+ if (flagged) return flagged;
66
+ if (values.indexOf(DEFAULT_MODEL) !== -1 || values.length === 0) return DEFAULT_MODEL;
67
+ return values[0];
68
+ }
69
+
40
70
  function selectTitleModel(models, sessionModel) {
41
71
  var entries = models.map(function (entry) {
42
72
  return typeof entry === "string" ? { value: entry } : entry;
43
73
  }).filter(function (entry) { return entry && typeof entry.value === "string" && entry.value.trim(); });
44
- var selected = entries.find(function (entry) { return entry.value === "gpt-5.6-luna"; })
74
+ var selected = null;
75
+ for (var ti = 0; ti < TITLE_MODELS.length && !selected; ti++) {
76
+ selected = entries.find(function (entry) { return entry.value === TITLE_MODELS[ti]; }) || null;
77
+ }
78
+ selected = selected
45
79
  || entries.find(function (entry) { return entry.value === sessionModel; }) || entries[0];
46
80
  if (!selected) throw new Error("No supported Codex title model is available");
47
81
  var efforts = selected.supportedEffortLevels;
@@ -49,4 +83,4 @@ function selectTitleModel(models, sessionModel) {
49
83
  return { model: selected.value, effort: effort };
50
84
  }
51
85
 
52
- module.exports = { fallback: fallback, load: load, selectTitleModel: selectTitleModel };
86
+ module.exports = { DEFAULT_MODEL: DEFAULT_MODEL, fallback: fallback, load: load, defaultModel: defaultModel, selectTitleModel: selectTitleModel };