openusage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/bin/openusage +91 -0
  2. package/package.json +33 -0
  3. package/plugins/amp/icon.svg +6 -0
  4. package/plugins/amp/plugin.js +175 -0
  5. package/plugins/amp/plugin.json +20 -0
  6. package/plugins/amp/plugin.test.js +365 -0
  7. package/plugins/antigravity/icon.svg +3 -0
  8. package/plugins/antigravity/plugin.js +484 -0
  9. package/plugins/antigravity/plugin.json +17 -0
  10. package/plugins/antigravity/plugin.test.js +1356 -0
  11. package/plugins/claude/icon.svg +3 -0
  12. package/plugins/claude/plugin.js +565 -0
  13. package/plugins/claude/plugin.json +28 -0
  14. package/plugins/claude/plugin.test.js +1012 -0
  15. package/plugins/codex/icon.svg +3 -0
  16. package/plugins/codex/plugin.js +673 -0
  17. package/plugins/codex/plugin.json +30 -0
  18. package/plugins/codex/plugin.test.js +1071 -0
  19. package/plugins/copilot/icon.svg +3 -0
  20. package/plugins/copilot/plugin.js +264 -0
  21. package/plugins/copilot/plugin.json +20 -0
  22. package/plugins/copilot/plugin.test.js +529 -0
  23. package/plugins/cursor/icon.svg +3 -0
  24. package/plugins/cursor/plugin.js +526 -0
  25. package/plugins/cursor/plugin.json +24 -0
  26. package/plugins/cursor/plugin.test.js +1168 -0
  27. package/plugins/factory/icon.svg +1 -0
  28. package/plugins/factory/plugin.js +407 -0
  29. package/plugins/factory/plugin.json +19 -0
  30. package/plugins/factory/plugin.test.js +833 -0
  31. package/plugins/gemini/icon.svg +4 -0
  32. package/plugins/gemini/plugin.js +413 -0
  33. package/plugins/gemini/plugin.json +20 -0
  34. package/plugins/gemini/plugin.test.js +735 -0
  35. package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
  36. package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
  37. package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
  38. package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
  39. package/plugins/kimi/icon.svg +3 -0
  40. package/plugins/kimi/plugin.js +358 -0
  41. package/plugins/kimi/plugin.json +19 -0
  42. package/plugins/kimi/plugin.test.js +619 -0
  43. package/plugins/minimax/icon.svg +4 -0
  44. package/plugins/minimax/plugin.js +388 -0
  45. package/plugins/minimax/plugin.json +17 -0
  46. package/plugins/minimax/plugin.test.js +943 -0
  47. package/plugins/perplexity/icon.svg +1 -0
  48. package/plugins/perplexity/plugin.js +378 -0
  49. package/plugins/perplexity/plugin.json +15 -0
  50. package/plugins/perplexity/plugin.test.js +602 -0
  51. package/plugins/windsurf/icon.svg +3 -0
  52. package/plugins/windsurf/plugin.js +218 -0
  53. package/plugins/windsurf/plugin.json +16 -0
  54. package/plugins/windsurf/plugin.test.js +455 -0
  55. package/plugins/zai/icon.svg +5 -0
  56. package/plugins/zai/plugin.js +156 -0
  57. package/plugins/zai/plugin.json +18 -0
  58. package/plugins/zai/plugin.test.js +396 -0
@@ -0,0 +1,4 @@
1
+ <svg width="100" height="100" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M96 8c3.6 0 6.8 2.4 7.7 5.9 5.4 21.2 19.2 35 40.4 40.4 3.5.9 5.9 4.1 5.9 7.7s-2.4 6.8-5.9 7.7c-21.2 5.4-35 19.2-40.4 40.4-.9 3.5-4.1 5.9-7.7 5.9s-6.8-2.4-7.7-5.9c-5.4-21.2-19.2-35-40.4-40.4C44.4 68.8 42 65.6 42 62s2.4-6.8 5.9-7.7c21.2-5.4 35-19.2 40.4-40.4C89.2 10.4 92.4 8 96 8z" fill="currentColor"/>
3
+ <path d="M160 88c1.8 0 3.4 1.2 3.8 2.9 2.6 10.2 9.3 16.9 19.5 19.5 1.7.4 2.9 2 2.9 3.8s-1.2 3.4-2.9 3.8c-10.2 2.6-16.9 9.3-19.5 19.5-.4 1.7-2 2.9-3.8 2.9s-3.4-1.2-3.8-2.9c-2.6-10.2-9.3-16.9-19.5-19.5-1.7-.4-2.9-2-2.9-3.8s1.2-3.4 2.9-3.8c10.2-2.6 16.9-9.3 19.5-19.5.4-1.7 2-2.9 3.8-2.9z" fill="currentColor"/>
4
+ </svg>
@@ -0,0 +1,413 @@
1
+ (function () {
2
+ const SETTINGS_PATH = "~/.gemini/settings.json"
3
+ const CREDS_PATH = "~/.gemini/oauth_creds.json"
4
+ const OAUTH2_JS_PATHS = [
5
+ "~/.bun/install/global/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js",
6
+ "~/.npm-global/lib/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js",
7
+ "~/.nvm/versions/node/current/lib/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js",
8
+ "/opt/homebrew/opt/gemini-cli/libexec/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js",
9
+ "/usr/local/opt/gemini-cli/libexec/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js",
10
+ ]
11
+
12
+ // Dynamic discovery for pnpm global installs (path varies by pnpm version)
13
+ function discoverPnpmOauth2Paths(ctx) {
14
+ const pnpmGlobal = "~/.local/share/pnpm/global"
15
+ if (!ctx.host.fs.exists(pnpmGlobal)) return []
16
+ try {
17
+ const entries = ctx.host.fs.listDir(pnpmGlobal)
18
+ const paths = []
19
+ for (let i = 0; i < entries.length; i += 1) {
20
+ const versionDir = pnpmGlobal + "/" + entries[i]
21
+ const pnpmDir = versionDir + "/.pnpm"
22
+ if (!ctx.host.fs.exists(pnpmDir)) continue
23
+ try {
24
+ const pnpmEntries = ctx.host.fs.listDir(pnpmDir)
25
+ for (let j = 0; j < pnpmEntries.length; j += 1) {
26
+ if (pnpmEntries[j].indexOf("@google+gemini-cli-core") === 0) {
27
+ const candidate = pnpmDir + "/" + pnpmEntries[j] +
28
+ "/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"
29
+ paths.push(candidate)
30
+ }
31
+ }
32
+ } catch (e) { /* ignore */ }
33
+ }
34
+ return paths
35
+ } catch (e) {
36
+ return []
37
+ }
38
+ }
39
+
40
+ const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
41
+ const QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota"
42
+ const PROJECTS_URL = "https://cloudresourcemanager.googleapis.com/v1/projects"
43
+ const TOKEN_URL = "https://oauth2.googleapis.com/token"
44
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000
45
+
46
+ const IDE_METADATA = {
47
+ ideType: "IDE_UNSPECIFIED",
48
+ platform: "PLATFORM_UNSPECIFIED",
49
+ pluginType: "GEMINI",
50
+ duetProject: "default",
51
+ }
52
+
53
+ function loadSettings(ctx) {
54
+ if (!ctx.host.fs.exists(SETTINGS_PATH)) return null
55
+ try {
56
+ return ctx.util.tryParseJson(ctx.host.fs.readText(SETTINGS_PATH))
57
+ } catch (e) {
58
+ ctx.host.log.warn("failed reading settings: " + String(e))
59
+ return null
60
+ }
61
+ }
62
+
63
+ function assertSupportedAuthType(ctx) {
64
+ const settings = loadSettings(ctx)
65
+ const authType =
66
+ settings && typeof settings.authType === "string" ? settings.authType.trim().toLowerCase() : null
67
+
68
+ if (!authType || authType === "oauth-personal") return
69
+ if (authType === "api-key") {
70
+ throw "Gemini auth type api-key is not supported by this plugin yet."
71
+ }
72
+ if (authType === "vertex-ai") {
73
+ throw "Gemini auth type vertex-ai is not supported by this plugin yet."
74
+ }
75
+ throw "Gemini unsupported auth type: " + authType
76
+ }
77
+
78
+ function loadOauthCreds(ctx) {
79
+ if (!ctx.host.fs.exists(CREDS_PATH)) return null
80
+ try {
81
+ const parsed = ctx.util.tryParseJson(ctx.host.fs.readText(CREDS_PATH))
82
+ if (!parsed || typeof parsed !== "object") return null
83
+ if (!parsed.access_token && !parsed.refresh_token) return null
84
+ return parsed
85
+ } catch (e) {
86
+ ctx.host.log.warn("failed reading creds: " + String(e))
87
+ return null
88
+ }
89
+ }
90
+
91
+ function saveOauthCreds(ctx, creds) {
92
+ try {
93
+ ctx.host.fs.writeText(CREDS_PATH, JSON.stringify(creds, null, 2))
94
+ } catch (e) {
95
+ ctx.host.log.warn("failed persisting creds: " + String(e))
96
+ }
97
+ }
98
+
99
+ function parseOauthClientCreds(text) {
100
+ if (!text || typeof text !== "string") return null
101
+ const idMatch = text.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/)
102
+ const secretMatch = text.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/)
103
+ if (!idMatch || !secretMatch) return null
104
+ return { clientId: idMatch[1], clientSecret: secretMatch[1] }
105
+ }
106
+
107
+ function loadOauthClientCreds(ctx) {
108
+ const allPaths = OAUTH2_JS_PATHS.concat(discoverPnpmOauth2Paths(ctx))
109
+ for (let i = 0; i < allPaths.length; i += 1) {
110
+ const path = allPaths[i]
111
+ if (!ctx.host.fs.exists(path)) continue
112
+ try {
113
+ const parsed = parseOauthClientCreds(ctx.host.fs.readText(path))
114
+ if (parsed) return parsed
115
+ } catch (e) {
116
+ ctx.host.log.warn("failed reading oauth2.js: " + String(e))
117
+ }
118
+ }
119
+ return null
120
+ }
121
+
122
+ function readNumber(value) {
123
+ const n = Number(value)
124
+ return Number.isFinite(n) ? n : null
125
+ }
126
+
127
+ function decodeIdToken(ctx, token) {
128
+ if (typeof token !== "string" || !token) return null
129
+ try {
130
+ const payload = ctx.jwt.decodePayload(token)
131
+ return payload && typeof payload === "object" ? payload : null
132
+ } catch {
133
+ return null
134
+ }
135
+ }
136
+
137
+ function needsRefresh(creds) {
138
+ if (!creds.access_token) return true
139
+ const expiry = readNumber(creds.expiry_date)
140
+ if (expiry === null) return false
141
+ const expiryMs = expiry > 10_000_000_000 ? expiry : expiry * 1000
142
+ return Date.now() + REFRESH_BUFFER_MS >= expiryMs
143
+ }
144
+
145
+ function refreshToken(ctx, creds) {
146
+ if (!creds.refresh_token) return null
147
+ const clientCreds = loadOauthClientCreds(ctx)
148
+ if (!clientCreds) return null
149
+
150
+ let resp
151
+ try {
152
+ resp = ctx.util.request({
153
+ method: "POST",
154
+ url: TOKEN_URL,
155
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
156
+ bodyText:
157
+ "client_id=" +
158
+ encodeURIComponent(clientCreds.clientId) +
159
+ "&client_secret=" +
160
+ encodeURIComponent(clientCreds.clientSecret) +
161
+ "&refresh_token=" +
162
+ encodeURIComponent(creds.refresh_token) +
163
+ "&grant_type=refresh_token",
164
+ timeoutMs: 15000,
165
+ })
166
+ } catch (e) {
167
+ ctx.host.log.warn("refresh request failed: " + String(e))
168
+ return null
169
+ }
170
+
171
+ if (ctx.util.isAuthStatus(resp.status)) {
172
+ throw "Gemini session expired. Run `gemini auth login` to authenticate."
173
+ }
174
+ if (resp.status < 200 || resp.status >= 300) return null
175
+
176
+ const data = ctx.util.tryParseJson(resp.bodyText)
177
+ if (!data || typeof data.access_token !== "string" || !data.access_token) return null
178
+
179
+ creds.access_token = data.access_token
180
+ if (typeof data.id_token === "string" && data.id_token) creds.id_token = data.id_token
181
+ if (typeof data.refresh_token === "string" && data.refresh_token) creds.refresh_token = data.refresh_token
182
+ if (typeof data.expires_in === "number") {
183
+ creds.expiry_date = Date.now() + data.expires_in * 1000
184
+ }
185
+
186
+ saveOauthCreds(ctx, creds)
187
+ return creds.access_token
188
+ }
189
+
190
+ function postJson(ctx, url, accessToken, body) {
191
+ return ctx.util.request({
192
+ method: "POST",
193
+ url,
194
+ headers: {
195
+ Authorization: "Bearer " + accessToken,
196
+ Accept: "application/json",
197
+ "Content-Type": "application/json",
198
+ },
199
+ bodyText: JSON.stringify(body || {}),
200
+ timeoutMs: 10000,
201
+ })
202
+ }
203
+
204
+ function readFirstStringDeep(value, keys) {
205
+ if (!value || typeof value !== "object") return null
206
+
207
+ for (let i = 0; i < keys.length; i += 1) {
208
+ const v = value[keys[i]]
209
+ if (typeof v === "string" && v.trim()) return v.trim()
210
+ }
211
+
212
+ const nested = Object.values(value)
213
+ for (let i = 0; i < nested.length; i += 1) {
214
+ const found = readFirstStringDeep(nested[i], keys)
215
+ if (found) return found
216
+ }
217
+ return null
218
+ }
219
+
220
+ function mapTierToPlan(tier, idTokenPayload) {
221
+ if (!tier) return null
222
+ const normalized = String(tier).trim().toLowerCase()
223
+ if (normalized === "standard-tier") return "Paid"
224
+ if (normalized === "legacy-tier") return "Legacy"
225
+ if (normalized === "free-tier") return idTokenPayload && idTokenPayload.hd ? "Workspace" : "Free"
226
+ return null
227
+ }
228
+
229
+ function discoverProjectId(ctx, accessToken, loadCodeAssistData) {
230
+ const fromLoadCodeAssist = readFirstStringDeep(loadCodeAssistData, ["cloudaicompanionProject"])
231
+ if (fromLoadCodeAssist) return fromLoadCodeAssist
232
+
233
+ let projectsResp
234
+ try {
235
+ projectsResp = ctx.util.request({
236
+ method: "GET",
237
+ url: PROJECTS_URL,
238
+ headers: { Authorization: "Bearer " + accessToken, Accept: "application/json" },
239
+ timeoutMs: 10000,
240
+ })
241
+ } catch (e) {
242
+ ctx.host.log.warn("project discovery failed: " + String(e))
243
+ return null
244
+ }
245
+
246
+ if (projectsResp.status < 200 || projectsResp.status >= 300) return null
247
+ const projectsData = ctx.util.tryParseJson(projectsResp.bodyText)
248
+ const projects = projectsData && Array.isArray(projectsData.projects) ? projectsData.projects : []
249
+ for (let i = 0; i < projects.length; i += 1) {
250
+ const project = projects[i]
251
+ const projectId = project && typeof project.projectId === "string" ? project.projectId : null
252
+ if (!projectId) continue
253
+ if (projectId.indexOf("gen-lang-client") === 0) return projectId
254
+ const labels = project && project.labels && typeof project.labels === "object" ? project.labels : null
255
+ if (labels && labels["generative-language"] !== undefined) return projectId
256
+ }
257
+ return null
258
+ }
259
+
260
+ function collectQuotaBuckets(value, out) {
261
+ if (Array.isArray(value)) {
262
+ for (let i = 0; i < value.length; i += 1) collectQuotaBuckets(value[i], out)
263
+ return
264
+ }
265
+ if (!value || typeof value !== "object") return
266
+
267
+ if (typeof value.remainingFraction === "number") {
268
+ const modelId =
269
+ typeof value.modelId === "string"
270
+ ? value.modelId
271
+ : typeof value.model_id === "string"
272
+ ? value.model_id
273
+ : "unknown"
274
+ out.push({
275
+ modelId,
276
+ remainingFraction: value.remainingFraction,
277
+ resetTime: value.resetTime || value.reset_time || null,
278
+ })
279
+ }
280
+
281
+ const nested = Object.values(value)
282
+ for (let i = 0; i < nested.length; i += 1) collectQuotaBuckets(nested[i], out)
283
+ }
284
+
285
+ function pickLowestRemainingBucket(buckets) {
286
+ let best = null
287
+ for (let i = 0; i < buckets.length; i += 1) {
288
+ const bucket = buckets[i]
289
+ if (!Number.isFinite(bucket.remainingFraction)) continue
290
+ if (!best || bucket.remainingFraction < best.remainingFraction) best = bucket
291
+ }
292
+ return best
293
+ }
294
+
295
+ function toUsageLine(ctx, label, bucket) {
296
+ const clampedRemaining = Math.max(0, Math.min(1, Number(bucket.remainingFraction)))
297
+ const used = Math.round((1 - clampedRemaining) * 100)
298
+ const resetsAt = ctx.util.toIso(bucket.resetTime)
299
+ const opts = {
300
+ label,
301
+ used,
302
+ limit: 100,
303
+ format: { kind: "percent" },
304
+ }
305
+ if (resetsAt) opts.resetsAt = resetsAt
306
+ return ctx.line.progress(opts)
307
+ }
308
+
309
+ function parseQuotaLines(ctx, quotaData) {
310
+ const buckets = []
311
+ collectQuotaBuckets(quotaData, buckets)
312
+ if (!buckets.length) return []
313
+
314
+ const proBuckets = []
315
+ const flashBuckets = []
316
+ for (let i = 0; i < buckets.length; i += 1) {
317
+ const bucket = buckets[i]
318
+ const lower = String(bucket.modelId || "").toLowerCase()
319
+ if (lower.indexOf("gemini") !== -1 && lower.indexOf("pro") !== -1) {
320
+ proBuckets.push(bucket)
321
+ } else if (lower.indexOf("gemini") !== -1 && lower.indexOf("flash") !== -1) {
322
+ flashBuckets.push(bucket)
323
+ }
324
+ }
325
+
326
+ const lines = []
327
+ const pro = pickLowestRemainingBucket(proBuckets)
328
+ if (pro) lines.push(toUsageLine(ctx, "Pro", pro))
329
+ const flash = pickLowestRemainingBucket(flashBuckets)
330
+ if (flash) lines.push(toUsageLine(ctx, "Flash", flash))
331
+ return lines
332
+ }
333
+
334
+ function fetchLoadCodeAssist(ctx, accessToken, creds) {
335
+ let currentToken = accessToken
336
+ const resp = ctx.util.retryOnceOnAuth({
337
+ request: function (token) {
338
+ return postJson(ctx, LOAD_CODE_ASSIST_URL, token || currentToken, { metadata: IDE_METADATA })
339
+ },
340
+ refresh: function () {
341
+ const refreshed = refreshToken(ctx, creds)
342
+ if (refreshed) currentToken = refreshed
343
+ return refreshed
344
+ },
345
+ })
346
+
347
+ if (ctx.util.isAuthStatus(resp.status)) {
348
+ throw "Gemini session expired. Run `gemini auth login` to authenticate."
349
+ }
350
+ if (resp.status < 200 || resp.status >= 300) return { data: null, accessToken: currentToken }
351
+ return { data: ctx.util.tryParseJson(resp.bodyText), accessToken: currentToken }
352
+ }
353
+
354
+ function fetchQuotaWithRetry(ctx, accessToken, creds, projectId) {
355
+ let currentToken = accessToken
356
+ const resp = ctx.util.retryOnceOnAuth({
357
+ request: function (token) {
358
+ const body = projectId ? { project: projectId } : {}
359
+ return postJson(ctx, QUOTA_URL, token || currentToken, body)
360
+ },
361
+ refresh: function () {
362
+ const refreshed = refreshToken(ctx, creds)
363
+ if (refreshed) currentToken = refreshed
364
+ return refreshed
365
+ },
366
+ })
367
+
368
+ if (ctx.util.isAuthStatus(resp.status)) {
369
+ throw "Gemini session expired. Run `gemini auth login` to authenticate."
370
+ }
371
+ if (resp.status < 200 || resp.status >= 300) {
372
+ throw "Gemini quota request failed (HTTP " + String(resp.status) + "). Try again later."
373
+ }
374
+ return resp
375
+ }
376
+
377
+ function probe(ctx) {
378
+ assertSupportedAuthType(ctx)
379
+
380
+ const creds = loadOauthCreds(ctx)
381
+ if (!creds) throw "Not logged in. Run `gemini auth login` to authenticate."
382
+
383
+ let accessToken = creds.access_token
384
+ if (needsRefresh(creds)) {
385
+ const refreshed = refreshToken(ctx, creds)
386
+ if (refreshed) accessToken = refreshed
387
+ else if (!accessToken) throw "Not logged in. Run `gemini auth login` to authenticate."
388
+ }
389
+
390
+ const idTokenPayload = decodeIdToken(ctx, creds.id_token)
391
+ const loadCodeAssistResult = fetchLoadCodeAssist(ctx, accessToken, creds)
392
+ accessToken = loadCodeAssistResult.accessToken
393
+
394
+ const tier = readFirstStringDeep(loadCodeAssistResult.data, ["tier", "userTier", "subscriptionTier"])
395
+ const plan = mapTierToPlan(tier, idTokenPayload)
396
+
397
+ const projectId = discoverProjectId(ctx, accessToken, loadCodeAssistResult.data)
398
+ const quotaResp = fetchQuotaWithRetry(ctx, accessToken, creds, projectId)
399
+ const quotaData = ctx.util.tryParseJson(quotaResp.bodyText)
400
+ if (!quotaData || typeof quotaData !== "object") {
401
+ throw "Gemini quota response invalid. Try again later."
402
+ }
403
+
404
+ const lines = parseQuotaLines(ctx, quotaData)
405
+ const email = idTokenPayload && typeof idTokenPayload.email === "string" ? idTokenPayload.email : null
406
+ if (email) lines.push(ctx.line.text({ label: "Account", value: email }))
407
+ if (!lines.length) lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
408
+
409
+ return { plan: plan || undefined, lines }
410
+ }
411
+
412
+ globalThis.__openusage_plugin = { id: "gemini", probe }
413
+ })()
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "gemini",
4
+ "name": "Gemini",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#4285F4",
9
+ "cli": {
10
+ "category": "cli",
11
+ "binaryName": "gemini",
12
+ "installCmd": "npm install -g @google/gemini-cli",
13
+ "loginCmd": "gemini auth login"
14
+ },
15
+ "lines": [
16
+ { "type": "progress", "label": "Pro", "scope": "overview", "primaryOrder": 1 },
17
+ { "type": "progress", "label": "Flash", "scope": "overview", "primaryOrder": 2 },
18
+ { "type": "text", "label": "Account", "scope": "detail" }
19
+ ]
20
+ }