agent-dealer 1.2.1 → 1.2.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.
Files changed (57) hide show
  1. package/bundle/server/dist/adapters/linear-inbox.js +9 -5
  2. package/bundle/server/dist/capacity/adapter.js +2 -0
  3. package/bundle/server/dist/capacity/claude-events.js +45 -10
  4. package/bundle/server/dist/capacity/claude-events.test.js +7 -0
  5. package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
  6. package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
  7. package/bundle/server/dist/capacity/codex-app-server.js +100 -10
  8. package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
  9. package/bundle/server/dist/capacity/cursor-individual-credentials.js +425 -0
  10. package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +369 -0
  11. package/bundle/server/dist/capacity/cursor-individual.js +1037 -0
  12. package/bundle/server/dist/capacity/cursor-individual.test.js +1081 -0
  13. package/bundle/server/dist/capacity/muse-host.js +748 -0
  14. package/bundle/server/dist/capacity/muse-host.test.js +480 -0
  15. package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
  16. package/bundle/server/dist/capacity/muse.js +389 -91
  17. package/bundle/server/dist/capacity/muse.test.js +100 -61
  18. package/bundle/server/dist/capacity/service.js +1 -0
  19. package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
  20. package/bundle/server/dist/db/index.js +30 -0
  21. package/bundle/server/dist/db/schema.sql +27 -0
  22. package/bundle/server/dist/index.js +17 -2
  23. package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
  24. package/bundle/server/dist/repository/repository-mappings.js +46 -0
  25. package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
  26. package/bundle/server/dist/repository/runtime-capacity.js +8 -3
  27. package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
  28. package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
  29. package/bundle/server/dist/routes/index.js +58 -9
  30. package/bundle/server/dist/routes/intake-linear.test.js +55 -1
  31. package/bundle/server/dist/routes/repository-mappings.js +22 -0
  32. package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
  33. package/bundle/server/dist/routes/runtime-capacity.test.js +84 -8
  34. package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
  35. package/bundle/server/dist/runners/muse-serve-session.js +313 -0
  36. package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
  37. package/bundle/server/package.json +2 -2
  38. package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
  39. package/bundle/server/static-ui/assets/index-CYqRh_-S.css +1 -0
  40. package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
  41. package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
  42. package/bundle/server/static-ui/index.html +2 -2
  43. package/bundle/shared/dist/index.d.ts +6 -6
  44. package/bundle/shared/dist/linear-intake.d.ts +71 -0
  45. package/bundle/shared/dist/linear-intake.js +90 -0
  46. package/bundle/shared/dist/linear-intake.test.js +80 -1
  47. package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
  48. package/bundle/shared/dist/runtime-capacity.js +60 -0
  49. package/bundle/shared/dist/runtime-capacity.test.js +1 -0
  50. package/bundle/shared/package.json +1 -1
  51. package/dist/doctor.d.ts +57 -0
  52. package/dist/doctor.js +211 -0
  53. package/dist/doctor.test.d.ts +1 -0
  54. package/dist/doctor.test.js +246 -0
  55. package/package.json +1 -1
  56. package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
  57. package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
@@ -0,0 +1,425 @@
1
+ // packages/server/src/capacity/cursor-individual-credentials.ts
2
+ //
3
+ // NOT-250: isolated credential lookup for the experimental Cursor Individual
4
+ // dashboard adapter. NOT-267: the desktop Cursor login is the primary
5
+ // source, the Cursor Agent auth file the fallback.
6
+ //
7
+ // The dashboard endpoints this feature calls are undocumented: the only
8
+ // credential available is the existing local Cursor login, whose storage
9
+ // locations and shapes are themselves not a supported contract and may
10
+ // change without notice. All of that risk lives in this one module:
11
+ //
12
+ // - Resolution order: Cursor desktop `state.vscdb` first (read-only,
13
+ // single allowlisted key), then the Cursor Agent `auth.json` allowlisted
14
+ // token shapes. The first usable source wins; Agent auth unchanged.
15
+ // - Accepted JSON shapes are an explicit allowlist below — anything else
16
+ // reads `unparsable` (changed format), never a guess.
17
+ // - `cursorIndividualCredentialStatus()` (the diagnostics surface) returns
18
+ // presence/source/path/format ONLY — never secret material.
19
+ // - `loadCursorIndividualCredential()` additionally returns an in-memory
20
+ // `authHeader` for the dashboard HTTP call. The header value must never be
21
+ // persisted, returned to the browser, or logged — the adapter, repository,
22
+ // and route layers enforce that, and tests pin it.
23
+ //
24
+ // No silent opt-in or credential migration lives here: this module only
25
+ // *finds* credentials; whether they may be *used* is decided by the
26
+ // explicit `AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY=experimental` opt-in in
27
+ // cursor-individual.ts, which short-circuits before touching this module.
28
+ // Nothing here writes Cursor's database, refreshes tokens, or touches the
29
+ // Keychain or auth files — all reads are read-only.
30
+ import Database from "better-sqlite3";
31
+ import fs from "node:fs";
32
+ import os from "node:os";
33
+ import path from "node:path";
34
+ /** Explicit credential-file override (tests point this at tmp fixtures). */
35
+ export const CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV = "CURSOR_INDIVIDUAL_CREDENTIAL_FILE";
36
+ /** Home-directory override for tests (mirrors how OS home resolution works). */
37
+ export const CURSOR_INDIVIDUAL_HOME_ENV = "CURSOR_INDIVIDUAL_HOME";
38
+ /** Explicit desktop-state database override (tests point this at tmp fixtures). */
39
+ export const CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV = "CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE";
40
+ /**
41
+ * Community-observed local Cursor login file. The location is undocumented
42
+ * and may move — when it does, reads degrade to `absent`/`unparsable`, never
43
+ * to a scan of unrelated files. Exactly one default candidate is listed on
44
+ * purpose: this module must not go hunting through the home directory.
45
+ */
46
+ export const CURSOR_INDIVIDUAL_DEFAULT_RELATIVE_PATH = path.join(".cursor", "auth.json");
47
+ /**
48
+ * Accepted top-level token keys. The credential format is undocumented, so
49
+ * both camelCase and snake_case spellings are tolerated — but ONLY these
50
+ * keys. An object with none of them is a changed format (`unparsable`).
51
+ */
52
+ export const CURSOR_INDIVIDUAL_TOKEN_KEYS = [
53
+ "token",
54
+ "accessToken",
55
+ "access_token",
56
+ "sessionToken",
57
+ "session_token",
58
+ "authToken",
59
+ "auth_token",
60
+ "apiKey",
61
+ "api_key",
62
+ "jwt",
63
+ ];
64
+ /**
65
+ * Accepted top-level WorkOS user-id keys. The dashboard's session cookie is
66
+ * `<userId>%3A%3A<token>` (see `cursorIndividualAuthHeader`) — a bare token
67
+ * is not enough. Same allowlist discipline as the token keys: an explicit
68
+ * field here wins; otherwise the id is derived from the token's own JWT
69
+ * `sub` claim (see `decodeJwtSubject`). Either source is normalized through
70
+ * `normalizeWorkosUserId` to strip a provider prefix.
71
+ */
72
+ export const CURSOR_INDIVIDUAL_USER_ID_KEYS = [
73
+ "userId",
74
+ "user_id",
75
+ "workosUserId",
76
+ "workos_user_id",
77
+ "sub",
78
+ ];
79
+ /** Nested objects tolerated one level deep (same key allowlists apply). */
80
+ const NESTED_KEYS = ["auth", "data", "user"];
81
+ function homeDir() {
82
+ const override = process.env[CURSOR_INDIVIDUAL_HOME_ENV]?.trim();
83
+ if (override)
84
+ return override;
85
+ return os.homedir();
86
+ }
87
+ /** Ordered candidate files: explicit override first, then the local login. */
88
+ export function cursorIndividualCredentialCandidates() {
89
+ const override = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV]?.trim();
90
+ if (override)
91
+ return [override];
92
+ return [path.join(homeDir(), CURSOR_INDIVIDUAL_DEFAULT_RELATIVE_PATH)];
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Cursor desktop login (NOT-267, primary source).
96
+ //
97
+ // Cursor desktop keeps its login in the standard VS Code global-storage
98
+ // SQLite database (`state.vscdb`), under the key
99
+ // `cursorAuth/accessToken`. Community tools (ai-usagebar and ports) read
100
+ // that same key first and fall back to a Cursor Agent `auth.json` — this
101
+ // module follows the same order.
102
+ // ---------------------------------------------------------------------------
103
+ /** The ONLY desktop-state key this module ever reads — never anything else. */
104
+ export const CURSOR_DESKTOP_ACCESS_TOKEN_KEY = "cursorAuth/accessToken";
105
+ /** Format label for a credential answered by the desktop state database. */
106
+ export const CURSOR_DESKTOP_FORMAT = "desktop:cursorAuth/accessToken";
107
+ /**
108
+ * Ordered desktop-state database candidates: exactly one default path per
109
+ * OS (plus the explicit file override when set). The module never hunts
110
+ * beyond these — an absent database degrades to the Agent auth fallback,
111
+ * never to a scan of unrelated files.
112
+ *
113
+ * - macOS: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`
114
+ * - Windows: `%APPDATA%/Cursor/User/globalStorage/state.vscdb`
115
+ * - Linux (and anything else): `~/.config/Cursor/User/globalStorage/state.vscdb`
116
+ */
117
+ export function cursorIndividualDesktopStateCandidates(opts = {}) {
118
+ const override = process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV]?.trim();
119
+ if (override)
120
+ return [override];
121
+ const platform = opts.platform ?? process.platform;
122
+ const home = opts.home ?? homeDir();
123
+ if (platform === "win32") {
124
+ // An explicit `appData` opt always wins (even an explicit empty string,
125
+ // which reads as no candidate). A present-but-blank APPDATA is the same
126
+ // as unset — fall back to the roaming default instead of resolving to [].
127
+ const envAppData = process.env.APPDATA?.trim();
128
+ const appData = opts.appData ?? (envAppData ? envAppData : path.join(home, "AppData", "Roaming"));
129
+ if (!appData)
130
+ return [];
131
+ return [path.join(appData, "Cursor", "User", "globalStorage", "state.vscdb")];
132
+ }
133
+ if (platform === "darwin") {
134
+ return [
135
+ path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb"),
136
+ ];
137
+ }
138
+ return [path.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb")];
139
+ }
140
+ /**
141
+ * Normalize one raw desktop-state value to a token. Cursor stores the
142
+ * access token as a plain-text JWT; a JSON-quoted wrapping is tolerated
143
+ * (same bytes, quoted), anything else is unusable — never a guess.
144
+ */
145
+ export function normalizeDesktopTokenValue(value) {
146
+ if (typeof value !== "string")
147
+ return null;
148
+ const trimmed = value.trim();
149
+ if (!trimmed)
150
+ return null;
151
+ if (trimmed.startsWith('"')) {
152
+ try {
153
+ const inner = JSON.parse(trimmed);
154
+ return typeof inner === "string" && inner.trim() ? inner.trim() : null;
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ return trimmed;
161
+ }
162
+ /**
163
+ * Open the desktop state database READ-ONLY and read ONLY the allowlisted
164
+ * access-token key. Never throws: a missing/locked database, a missing key,
165
+ * or an unusable value all read null. The database is never copied, never
166
+ * written, and no unrelated key is ever selected.
167
+ */
168
+ export function defaultReadDesktopAccessToken(dbPath) {
169
+ let db = null;
170
+ try {
171
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
172
+ const row = db
173
+ .prepare("SELECT value FROM ItemTable WHERE key = ?")
174
+ .get(CURSOR_DESKTOP_ACCESS_TOKEN_KEY);
175
+ if (!row)
176
+ return null;
177
+ return normalizeDesktopTokenValue(row.value);
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ finally {
183
+ try {
184
+ db?.close();
185
+ }
186
+ catch {
187
+ // Read-only handle cleanup is best-effort; the read verdict stands.
188
+ }
189
+ }
190
+ }
191
+ /**
192
+ * Build a dashboard auth header from a desktop access token. The desktop
193
+ * store carries no separate user-id field, so the id is derived from the
194
+ * token's own JWT `sub` claim through the existing normalization — a token
195
+ * with no derivable id is unusable (null), never a Bearer-only guess.
196
+ */
197
+ export function desktopTokenToAuthHeader(token) {
198
+ const rawUserId = decodeJwtSubject(token);
199
+ if (!rawUserId)
200
+ return null;
201
+ const userId = normalizeWorkosUserId(rawUserId);
202
+ if (!userId)
203
+ return null;
204
+ return cursorIndividualAuthHeader(userId, token);
205
+ }
206
+ function pickByKeys(obj, keys) {
207
+ for (const key of keys) {
208
+ const value = obj[key];
209
+ if (typeof value === "string" && value.trim())
210
+ return { key, value: value.trim() };
211
+ }
212
+ for (const nest of NESTED_KEYS) {
213
+ const inner = obj[nest];
214
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) {
215
+ for (const key of keys) {
216
+ const value = inner[key];
217
+ if (typeof value === "string" && value.trim()) {
218
+ return { key: `${nest}.${key}`, value: value.trim() };
219
+ }
220
+ }
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ function pickToken(obj) {
226
+ const picked = pickByKeys(obj, CURSOR_INDIVIDUAL_TOKEN_KEYS);
227
+ return picked ? { key: picked.key, token: picked.value } : null;
228
+ }
229
+ function pickUserId(obj) {
230
+ return pickByKeys(obj, CURSOR_INDIVIDUAL_USER_ID_KEYS)?.value ?? null;
231
+ }
232
+ function base64UrlDecode(segment) {
233
+ try {
234
+ const padded = segment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(segment.length / 4) * 4, "=");
235
+ return Buffer.from(padded, "base64").toString("utf8");
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ /**
242
+ * The WorkOS session id is normally the JWT's own `sub` claim — decoded
243
+ * locally (no network, no signature verification: this only *reads* an id
244
+ * already present in a credential the caller trusts) so a plain access-token
245
+ * file still yields a usable id without a separate stored field. WorkOS
246
+ * subjects are commonly provider-prefixed (e.g. `google-oauth2|user_abc`);
247
+ * the raw claim is returned as-is here — `normalizeWorkosUserId` strips the
248
+ * prefix, applied uniformly to both this and an explicit `userId` field so
249
+ * either source lands on the same bare id.
250
+ */
251
+ export function decodeJwtSubject(token) {
252
+ const parts = token.split(".");
253
+ if (parts.length !== 3)
254
+ return null;
255
+ const json = base64UrlDecode(parts[1]);
256
+ if (json === null)
257
+ return null;
258
+ try {
259
+ const payload = JSON.parse(json);
260
+ if (!payload || typeof payload !== "object")
261
+ return null;
262
+ const sub = payload.sub;
263
+ return typeof sub === "string" && sub.trim() ? sub.trim() : null;
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ /**
270
+ * Strips a WorkOS connection-type prefix (`google-oauth2|user_abc` →
271
+ * `user_abc`): the dashboard's own session cookie carries the bare id after
272
+ * the final `|`, never the provider-qualified subject.
273
+ */
274
+ export function normalizeWorkosUserId(id) {
275
+ const at = id.lastIndexOf("|");
276
+ return at === -1 ? id : id.slice(at + 1);
277
+ }
278
+ /**
279
+ * Dashboard auth scheme: a `WorkosCursorSessionToken` cookie of
280
+ * `<userId>%3A%3A<token>` (community-observed: cursor-pulse, oh-my-pi,
281
+ * vct-core) — the dashboard rejects a Bearer `Authorization` header, and the
282
+ * `::` delimiter itself is percent-encoded in the live cookie value, not
283
+ * sent literally. The scheme is part of the undocumented surface and may
284
+ * drift (drift reads `unparsable`/`forbidden` at the HTTP layer, never a
285
+ * credential guess here). Returns the `Cookie` header VALUE (the caller
286
+ * sends it under the `Cookie` header name).
287
+ */
288
+ export function cursorIndividualAuthHeader(userId, token) {
289
+ return `WorkosCursorSessionToken=${userId}%3A%3A${token}`;
290
+ }
291
+ function defaultReadFile(filePath) {
292
+ try {
293
+ return fs.readFileSync(filePath, "utf8");
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ /**
300
+ * Find a usable local Cursor credential, desktop first, Agent auth as the
301
+ * fallback. Never throws: unreadable files, invalid JSON, unknown shapes,
302
+ * and unusable desktop tokens all map to `absent`/`unparsable`.
303
+ * The raw token leaves this module only inside `authHeader`, held in memory
304
+ * for the single dashboard call the adapter makes with it.
305
+ *
306
+ * When `CURSOR_INDIVIDUAL_CREDENTIAL_FILE` pins an explicit file, ONLY that
307
+ * file is consulted (legacy single-source behavior for fixtures); otherwise
308
+ * the desktop state database is tried first and the Agent auth file second.
309
+ */
310
+ export function loadCursorIndividualCredential(readFile = defaultReadFile, readDesktopToken = defaultReadDesktopAccessToken) {
311
+ const explicitFile = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV]?.trim();
312
+ // A present-but-unusable desktop login is remembered (not returned yet):
313
+ // the Agent fallback still gets its chance — any usable credential wins
314
+ // over a precise-but-unusable verdict. When nothing is usable, the
315
+ // desktop verdict is the most precise diagnostic, so it wins over the
316
+ // Agent one.
317
+ let desktopUnparsable = null;
318
+ if (!explicitFile) {
319
+ const desktop = loadDesktopCredential(readDesktopToken);
320
+ if (desktop?.found)
321
+ return desktop.found;
322
+ desktopUnparsable = desktop?.unparsable ?? null;
323
+ }
324
+ const agentUnparsable = (candidate, format) => desktopUnparsable ?? { status: "unparsable", source: null, path: candidate, format };
325
+ for (const candidate of cursorIndividualCredentialCandidates()) {
326
+ const raw = readFile(candidate);
327
+ if (raw === null)
328
+ continue;
329
+ let parsed;
330
+ try {
331
+ parsed = JSON.parse(raw);
332
+ }
333
+ catch {
334
+ return agentUnparsable(candidate, null);
335
+ }
336
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
337
+ return agentUnparsable(candidate, null);
338
+ }
339
+ const picked = pickToken(parsed);
340
+ if (!picked)
341
+ return agentUnparsable(candidate, null);
342
+ // The cookie needs a user id too — an explicit field wins; otherwise
343
+ // derive it from the token's own JWT `sub` claim. Neither present means
344
+ // this credential cannot build a usable session, same as any other
345
+ // format drift. Either source may be provider-prefixed
346
+ // (`google-oauth2|user_abc`) — normalize both the same way.
347
+ const rawUserId = pickUserId(parsed) ?? decodeJwtSubject(picked.token);
348
+ if (!rawUserId)
349
+ return agentUnparsable(candidate, picked.key);
350
+ // Re-check AFTER normalization: an id ending in `|` (e.g. a bare
351
+ // `"google-oauth2|"` with nothing after it) is non-empty here but
352
+ // normalizes to "" — that must degrade too, not build a cookie with an
353
+ // empty user id.
354
+ const userId = normalizeWorkosUserId(rawUserId);
355
+ if (!userId)
356
+ return agentUnparsable(candidate, picked.key);
357
+ return {
358
+ status: "found",
359
+ source: "agent",
360
+ path: candidate,
361
+ format: picked.key,
362
+ authHeader: cursorIndividualAuthHeader(userId, picked.token),
363
+ };
364
+ }
365
+ return desktopUnparsable ?? { status: "absent", source: null, path: null, format: null };
366
+ }
367
+ /**
368
+ * Try the desktop state database. Returns null when no desktop database
369
+ * was readable at all (fall through to Agent auth); otherwise the desktop
370
+ * verdict — `found` when the access token yields a usable session, or an
371
+ * `unparsable` diagnostic when the database answered but the token cannot
372
+ * (missing key, empty value, no derivable user id). A present-but-unusable
373
+ * desktop login is format drift, not absence — but the Agent fallback still
374
+ * gets its chance via the caller, which prefers any usable credential.
375
+ */
376
+ function loadDesktopCredential(readDesktopToken) {
377
+ let sawDatabase = false;
378
+ let unparsable = null;
379
+ for (const dbPath of cursorIndividualDesktopStateCandidates()) {
380
+ let token;
381
+ try {
382
+ token = readDesktopToken(dbPath);
383
+ }
384
+ catch {
385
+ continue;
386
+ }
387
+ if (token === null)
388
+ continue;
389
+ sawDatabase = true;
390
+ const authHeader = desktopTokenToAuthHeader(token);
391
+ if (authHeader) {
392
+ return {
393
+ found: {
394
+ status: "found",
395
+ source: "desktop",
396
+ path: dbPath,
397
+ format: CURSOR_DESKTOP_FORMAT,
398
+ authHeader,
399
+ },
400
+ unparsable: null,
401
+ };
402
+ }
403
+ // Token present but no derivable user id — same verdict as an Agent
404
+ // token with no id: changed format, never a Bearer-only guess.
405
+ if (!unparsable) {
406
+ unparsable = { status: "unparsable", source: null, path: dbPath, format: CURSOR_DESKTOP_FORMAT };
407
+ }
408
+ }
409
+ if (!sawDatabase && !unparsable)
410
+ return null;
411
+ return { found: null, unparsable };
412
+ }
413
+ /**
414
+ * Diagnostics-safe credential presence: delegates to the loader, then
415
+ * strips the in-memory secret. Safe to expose via logs and API responses.
416
+ */
417
+ export function cursorIndividualCredentialStatus(readFile = defaultReadFile, readDesktopToken = defaultReadDesktopAccessToken) {
418
+ const credential = loadCursorIndividualCredential(readFile, readDesktopToken);
419
+ return {
420
+ present: credential.status === "found",
421
+ source: credential.source,
422
+ path: credential.path,
423
+ format: credential.format,
424
+ };
425
+ }