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.
- package/bin/openusage +91 -0
- package/package.json +33 -0
- package/plugins/amp/icon.svg +6 -0
- package/plugins/amp/plugin.js +175 -0
- package/plugins/amp/plugin.json +20 -0
- package/plugins/amp/plugin.test.js +365 -0
- package/plugins/antigravity/icon.svg +3 -0
- package/plugins/antigravity/plugin.js +484 -0
- package/plugins/antigravity/plugin.json +17 -0
- package/plugins/antigravity/plugin.test.js +1356 -0
- package/plugins/claude/icon.svg +3 -0
- package/plugins/claude/plugin.js +565 -0
- package/plugins/claude/plugin.json +28 -0
- package/plugins/claude/plugin.test.js +1012 -0
- package/plugins/codex/icon.svg +3 -0
- package/plugins/codex/plugin.js +673 -0
- package/plugins/codex/plugin.json +30 -0
- package/plugins/codex/plugin.test.js +1071 -0
- package/plugins/copilot/icon.svg +3 -0
- package/plugins/copilot/plugin.js +264 -0
- package/plugins/copilot/plugin.json +20 -0
- package/plugins/copilot/plugin.test.js +529 -0
- package/plugins/cursor/icon.svg +3 -0
- package/plugins/cursor/plugin.js +526 -0
- package/plugins/cursor/plugin.json +24 -0
- package/plugins/cursor/plugin.test.js +1168 -0
- package/plugins/factory/icon.svg +1 -0
- package/plugins/factory/plugin.js +407 -0
- package/plugins/factory/plugin.json +19 -0
- package/plugins/factory/plugin.test.js +833 -0
- package/plugins/gemini/icon.svg +4 -0
- package/plugins/gemini/plugin.js +413 -0
- package/plugins/gemini/plugin.json +20 -0
- package/plugins/gemini/plugin.test.js +735 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
- package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
- package/plugins/kimi/icon.svg +3 -0
- package/plugins/kimi/plugin.js +358 -0
- package/plugins/kimi/plugin.json +19 -0
- package/plugins/kimi/plugin.test.js +619 -0
- package/plugins/minimax/icon.svg +4 -0
- package/plugins/minimax/plugin.js +388 -0
- package/plugins/minimax/plugin.json +17 -0
- package/plugins/minimax/plugin.test.js +943 -0
- package/plugins/perplexity/icon.svg +1 -0
- package/plugins/perplexity/plugin.js +378 -0
- package/plugins/perplexity/plugin.json +15 -0
- package/plugins/perplexity/plugin.test.js +602 -0
- package/plugins/windsurf/icon.svg +3 -0
- package/plugins/windsurf/plugin.js +218 -0
- package/plugins/windsurf/plugin.json +16 -0
- package/plugins/windsurf/plugin.test.js +455 -0
- package/plugins/zai/icon.svg +5 -0
- package/plugins/zai/plugin.js +156 -0
- package/plugins/zai/plugin.json +18 -0
- package/plugins/zai/plugin.test.js +396 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
|
|
2
|
+
<path fill="currentColor" d="M13.476 21.473c7.78-4.43 18.478-6.592 27.663-6.592 26.907 0 43.98 12.535 43.98 29.5 0 13.507-11.778 21.828-27.447 21.828-12.643 0-22.044-4.647-22.044-10.806 0-5.835 5.943-8.645 14.372-5.403l2.161-6.592c-13.075-4.214-23.88 1.73-23.88 11.995s12.75 17.613 29.823 17.613c20.315 0 35.12-11.238 35.12-28.635 0-21.72-20.532-37.604-52.085-37.604-10.157 0-21.611 2.593-31.013 7.672zm73.048 57.054c-7.78 4.43-18.478 6.592-27.663 6.592-26.907 0-43.98-12.535-43.98-29.5 0-13.507 11.778-21.828 27.447-21.828 12.643 0 22.044 4.647 22.044 10.806 0 5.835-5.943 8.645-14.372 5.403l-2.161 6.592c13.075 4.214 23.88-1.73 23.88-11.995S58.97 26.984 41.897 26.984c-20.315 0-35.12 11.238-35.12 28.635 0 21.72 20.532 37.604 52.085 37.604 10.157 0 21.611-2.593 31.013-7.672z" />
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
var QUOTA_FILENAME = "AIAssistantQuotaManager2.xml"
|
|
3
|
+
|
|
4
|
+
// JetBrains stores AI quota in 1e-5 credit units; divide by this to get credits.
|
|
5
|
+
var CREDIT_UNIT_SCALE = 100000
|
|
6
|
+
|
|
7
|
+
var PRODUCT_PREFIXES = [
|
|
8
|
+
"Aqua",
|
|
9
|
+
"AndroidStudio",
|
|
10
|
+
"CLion",
|
|
11
|
+
"DataGrip",
|
|
12
|
+
"DataSpell",
|
|
13
|
+
"GoLand",
|
|
14
|
+
"IdeaIC",
|
|
15
|
+
"IntelliJIdea",
|
|
16
|
+
"IntelliJIdeaCE",
|
|
17
|
+
"PhpStorm",
|
|
18
|
+
"PyCharm",
|
|
19
|
+
"PyCharmCE",
|
|
20
|
+
"Rider",
|
|
21
|
+
"RubyMine",
|
|
22
|
+
"RustRover",
|
|
23
|
+
"WebStorm",
|
|
24
|
+
"Writerside",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
function platformBaseDirs(platform) {
|
|
28
|
+
if (platform === "macos") {
|
|
29
|
+
return ["~/Library/Application Support/JetBrains"]
|
|
30
|
+
}
|
|
31
|
+
if (platform === "linux") {
|
|
32
|
+
return ["~/.config/JetBrains"]
|
|
33
|
+
}
|
|
34
|
+
if (platform === "windows") {
|
|
35
|
+
return ["~/AppData/Roaming/JetBrains"]
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
"~/Library/Application Support/JetBrains",
|
|
39
|
+
"~/.config/JetBrains",
|
|
40
|
+
"~/AppData/Roaming/JetBrains",
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isLikelyIdeDirName(name) {
|
|
45
|
+
if (typeof name !== "string") return false
|
|
46
|
+
var trimmed = name.trim()
|
|
47
|
+
if (!trimmed) return false
|
|
48
|
+
var hasPrefix = false
|
|
49
|
+
for (var i = 0; i < PRODUCT_PREFIXES.length; i += 1) {
|
|
50
|
+
if (trimmed.indexOf(PRODUCT_PREFIXES[i]) === 0) {
|
|
51
|
+
hasPrefix = true
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!hasPrefix) return false
|
|
56
|
+
return /\d{4}\.\d/.test(trimmed)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeListDir(ctx, path) {
|
|
60
|
+
if (
|
|
61
|
+
!ctx.host.fs ||
|
|
62
|
+
typeof ctx.host.fs.listDir !== "function" ||
|
|
63
|
+
!ctx.host.fs.exists(path)
|
|
64
|
+
) {
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
var entries = ctx.host.fs.listDir(path)
|
|
70
|
+
return Array.isArray(entries) ? entries : []
|
|
71
|
+
} catch (e) {
|
|
72
|
+
ctx.host.log.warn("listDir failed for " + path + ": " + String(e))
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildQuotaPaths(ctx) {
|
|
78
|
+
var bases = platformBaseDirs(ctx.app.platform)
|
|
79
|
+
var paths = []
|
|
80
|
+
var seen = Object.create(null)
|
|
81
|
+
for (var b = 0; b < bases.length; b += 1) {
|
|
82
|
+
var base = bases[b]
|
|
83
|
+
var entries = safeListDir(ctx, base)
|
|
84
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
85
|
+
var dirName = entries[i]
|
|
86
|
+
if (!isLikelyIdeDirName(dirName)) continue
|
|
87
|
+
var quotaPath = base + "/" + dirName + "/options/" + QUOTA_FILENAME
|
|
88
|
+
if (!ctx.host.fs.exists(quotaPath)) continue
|
|
89
|
+
if (!seen[quotaPath]) {
|
|
90
|
+
seen[quotaPath] = true
|
|
91
|
+
paths.push(quotaPath)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return paths
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeXmlEntities(text) {
|
|
99
|
+
if (!text) return ""
|
|
100
|
+
return String(text)
|
|
101
|
+
.replace(/ /g, "\n")
|
|
102
|
+
.replace(/ /g, "\r")
|
|
103
|
+
.replace(/"/g, '"')
|
|
104
|
+
.replace(/'/g, "'")
|
|
105
|
+
.replace(/</g, "<")
|
|
106
|
+
.replace(/>/g, ">")
|
|
107
|
+
.replace(/&/g, "&")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseOptionJson(ctx, xml, optionName) {
|
|
111
|
+
var elemMatch = xml.match(new RegExp('<option\\b[^>]*\\bname="' + optionName + '"[^>]*/>'))
|
|
112
|
+
if (!elemMatch) return null
|
|
113
|
+
var valueMatch = elemMatch[0].match(/\bvalue="([^"]*)"/)
|
|
114
|
+
if (!valueMatch) return null
|
|
115
|
+
return ctx.util.tryParseJson(decodeXmlEntities(valueMatch[1]))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toNumber(value) {
|
|
119
|
+
var n = Number(value)
|
|
120
|
+
return Number.isFinite(n) ? n : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function clamp(value, min, max) {
|
|
124
|
+
if (value < min) return min
|
|
125
|
+
if (value > max) return max
|
|
126
|
+
return value
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeQuota(quotaInfo) {
|
|
130
|
+
if (!quotaInfo || typeof quotaInfo !== "object") return null
|
|
131
|
+
|
|
132
|
+
var maximum = toNumber(quotaInfo.maximum)
|
|
133
|
+
var used = toNumber(quotaInfo.current)
|
|
134
|
+
var remaining = toNumber(quotaInfo.available)
|
|
135
|
+
|
|
136
|
+
var tariff = quotaInfo.tariffQuota && typeof quotaInfo.tariffQuota === "object"
|
|
137
|
+
? quotaInfo.tariffQuota
|
|
138
|
+
: null
|
|
139
|
+
var topUp = quotaInfo.topUpQuota && typeof quotaInfo.topUpQuota === "object"
|
|
140
|
+
? quotaInfo.topUpQuota
|
|
141
|
+
: null
|
|
142
|
+
|
|
143
|
+
if (maximum === null) {
|
|
144
|
+
var tariffMaximum = tariff ? toNumber(tariff.maximum) : null
|
|
145
|
+
var topUpMaximum = topUp ? toNumber(topUp.maximum) : null
|
|
146
|
+
if (tariffMaximum !== null || topUpMaximum !== null) {
|
|
147
|
+
maximum = (tariffMaximum !== null ? tariffMaximum : 0) + (topUpMaximum !== null ? topUpMaximum : 0)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (used === null) {
|
|
152
|
+
var tariffUsed = tariff ? toNumber(tariff.current) : null
|
|
153
|
+
var topUpUsed = topUp ? toNumber(topUp.current) : null
|
|
154
|
+
if (tariffUsed !== null || topUpUsed !== null) {
|
|
155
|
+
used = (tariffUsed !== null ? tariffUsed : 0) + (topUpUsed !== null ? topUpUsed : 0)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (remaining === null) {
|
|
160
|
+
var tariffRemaining = tariff ? toNumber(tariff.available) : null
|
|
161
|
+
var topUpRemaining = topUp ? toNumber(topUp.available) : null
|
|
162
|
+
if (tariffRemaining !== null || topUpRemaining !== null) {
|
|
163
|
+
remaining = (tariffRemaining !== null ? tariffRemaining : 0) + (topUpRemaining !== null ? topUpRemaining : 0)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (remaining === null && maximum !== null && used !== null) {
|
|
168
|
+
remaining = maximum - used
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (maximum === null || maximum <= 0 || used === null) return null
|
|
172
|
+
|
|
173
|
+
used = clamp(used, 0, maximum)
|
|
174
|
+
if (remaining !== null) remaining = clamp(remaining, 0, maximum)
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
used: used,
|
|
178
|
+
maximum: maximum,
|
|
179
|
+
remaining: remaining,
|
|
180
|
+
until: quotaInfo.until || null,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readQuotaState(ctx, path) {
|
|
185
|
+
if (!ctx.host.fs.exists(path)) return null
|
|
186
|
+
try {
|
|
187
|
+
var xml = ctx.host.fs.readText(path)
|
|
188
|
+
var quotaInfo = parseOptionJson(ctx, xml, "quotaInfo")
|
|
189
|
+
var nextRefill = parseOptionJson(ctx, xml, "nextRefill")
|
|
190
|
+
var quota = normalizeQuota(quotaInfo)
|
|
191
|
+
if (!quota) return null
|
|
192
|
+
return { path: path, quota: quota, nextRefill: nextRefill }
|
|
193
|
+
} catch (e) {
|
|
194
|
+
ctx.host.log.warn("failed reading quota state " + path + ": " + String(e))
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handles only simple single-component durations (PTnH, PnD, PnW).
|
|
200
|
+
// JetBrains currently uses values like PT720H or P30D.
|
|
201
|
+
function parseIsoDurationMs(value) {
|
|
202
|
+
if (typeof value !== "string" || !value) return null
|
|
203
|
+
|
|
204
|
+
var h = value.match(/^PT(\d+)H$/)
|
|
205
|
+
if (h) return Number(h[1]) * 60 * 60 * 1000
|
|
206
|
+
|
|
207
|
+
var d = value.match(/^P(\d+)D$/)
|
|
208
|
+
if (d) return Number(d[1]) * 24 * 60 * 60 * 1000
|
|
209
|
+
|
|
210
|
+
var w = value.match(/^P(\d+)W$/)
|
|
211
|
+
if (w) return Number(w[1]) * 7 * 24 * 60 * 60 * 1000
|
|
212
|
+
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function pickBestState(ctx, states) {
|
|
217
|
+
var best = null
|
|
218
|
+
var bestMs = -Infinity
|
|
219
|
+
|
|
220
|
+
for (var i = 0; i < states.length; i += 1) {
|
|
221
|
+
var state = states[i]
|
|
222
|
+
var untilMs = null
|
|
223
|
+
|
|
224
|
+
if (state.quota.until) {
|
|
225
|
+
untilMs = ctx.util.parseDateMs(state.quota.until)
|
|
226
|
+
}
|
|
227
|
+
if (untilMs === null && state.nextRefill && state.nextRefill.next) {
|
|
228
|
+
untilMs = ctx.util.parseDateMs(state.nextRefill.next)
|
|
229
|
+
}
|
|
230
|
+
if (untilMs === null) untilMs = -Infinity
|
|
231
|
+
|
|
232
|
+
if (!best || untilMs > bestMs) {
|
|
233
|
+
best = state
|
|
234
|
+
bestMs = untilMs
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (untilMs === bestMs) {
|
|
239
|
+
var currentRatio =
|
|
240
|
+
state.quota.maximum > 0 ? state.quota.used / state.quota.maximum : 0
|
|
241
|
+
var bestRatio =
|
|
242
|
+
best.quota.maximum > 0 ? best.quota.used / best.quota.maximum : 0
|
|
243
|
+
if (currentRatio > bestRatio) {
|
|
244
|
+
best = state
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
if (currentRatio === bestRatio && state.quota.used > best.quota.used) {
|
|
248
|
+
best = state
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return best
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatDecimal(value, places) {
|
|
258
|
+
if (!Number.isFinite(value)) return null
|
|
259
|
+
var factor = Math.pow(10, places)
|
|
260
|
+
var rounded = Math.round(value * factor) / factor
|
|
261
|
+
return rounded.toFixed(places).replace(/\.?0+$/, "")
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function detectDisplayScale(quota, nextRefill) {
|
|
265
|
+
var maxAbs = Math.max(
|
|
266
|
+
Math.abs(quota.maximum || 0),
|
|
267
|
+
Math.abs(quota.used || 0),
|
|
268
|
+
Math.abs(quota.remaining || 0)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if (nextRefill && nextRefill.tariff && typeof nextRefill.tariff === "object") {
|
|
272
|
+
var tariffAmount = toNumber(nextRefill.tariff.amount)
|
|
273
|
+
if (tariffAmount !== null) {
|
|
274
|
+
maxAbs = Math.max(maxAbs, Math.abs(tariffAmount))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (maxAbs >= CREDIT_UNIT_SCALE) return CREDIT_UNIT_SCALE
|
|
279
|
+
return 1
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function probe(ctx) {
|
|
283
|
+
var paths = buildQuotaPaths(ctx)
|
|
284
|
+
var states = []
|
|
285
|
+
|
|
286
|
+
for (var i = 0; i < paths.length; i += 1) {
|
|
287
|
+
var state = readQuotaState(ctx, paths[i])
|
|
288
|
+
if (state) states.push(state)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (states.length === 0) {
|
|
292
|
+
throw paths.length > 0
|
|
293
|
+
? "JetBrains AI Assistant quota data unavailable. Open AI Assistant once and try again."
|
|
294
|
+
: "JetBrains AI Assistant not detected. Open a JetBrains IDE with AI Assistant enabled."
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var chosen = pickBestState(ctx, states)
|
|
298
|
+
var quota = chosen.quota
|
|
299
|
+
var scale = detectDisplayScale(quota, chosen.nextRefill)
|
|
300
|
+
var usedPercent = (quota.used / quota.maximum) * 100
|
|
301
|
+
if (!Number.isFinite(usedPercent)) usedPercent = 0
|
|
302
|
+
usedPercent = clamp(usedPercent, 0, 100)
|
|
303
|
+
var line = {
|
|
304
|
+
label: "Quota",
|
|
305
|
+
used: usedPercent,
|
|
306
|
+
limit: 100,
|
|
307
|
+
format: { kind: "percent" },
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
var resetSource = null
|
|
311
|
+
if (chosen.nextRefill && chosen.nextRefill.next) {
|
|
312
|
+
resetSource = chosen.nextRefill.next
|
|
313
|
+
} else if (quota.until) {
|
|
314
|
+
resetSource = quota.until
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
var resetsAt = ctx.util.toIso(resetSource)
|
|
318
|
+
if (resetsAt) line.resetsAt = resetsAt
|
|
319
|
+
|
|
320
|
+
var duration = null
|
|
321
|
+
if (
|
|
322
|
+
chosen.nextRefill &&
|
|
323
|
+
chosen.nextRefill.tariff &&
|
|
324
|
+
typeof chosen.nextRefill.tariff === "object"
|
|
325
|
+
) {
|
|
326
|
+
duration = parseIsoDurationMs(chosen.nextRefill.tariff.duration)
|
|
327
|
+
}
|
|
328
|
+
if (duration) line.periodDurationMs = duration
|
|
329
|
+
|
|
330
|
+
var lines = [ctx.line.progress(line)]
|
|
331
|
+
|
|
332
|
+
lines.push(
|
|
333
|
+
ctx.line.text({
|
|
334
|
+
label: "Used",
|
|
335
|
+
value:
|
|
336
|
+
scale > 1
|
|
337
|
+
? formatDecimal(quota.used / scale, 2) + " / " + formatDecimal(quota.maximum / scale, 2) + " credits"
|
|
338
|
+
: String(quota.used),
|
|
339
|
+
})
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if (quota.remaining !== null) {
|
|
343
|
+
lines.push(
|
|
344
|
+
ctx.line.text({
|
|
345
|
+
label: "Remaining",
|
|
346
|
+
value: scale > 1 ? formatDecimal(quota.remaining / scale, 2) + " credits" : String(quota.remaining),
|
|
347
|
+
})
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
ctx.host.log.info("quota loaded from " + chosen.path)
|
|
352
|
+
|
|
353
|
+
return { lines: lines }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
globalThis.__openusage_plugin = { id: "jetbrains-ai-assistant", probe: probe }
|
|
357
|
+
})()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "jetbrains-ai-assistant",
|
|
4
|
+
"name": "JetBrains AI Assistant",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"entry": "plugin.js",
|
|
7
|
+
"icon": "icon.svg",
|
|
8
|
+
"brandColor": "#7d5fe6",
|
|
9
|
+
"cli": {
|
|
10
|
+
"category": "ide"
|
|
11
|
+
},
|
|
12
|
+
"lines": [
|
|
13
|
+
{ "type": "progress", "label": "Quota", "scope": "overview", "primaryOrder": 1 },
|
|
14
|
+
{ "type": "text", "label": "Used", "scope": "detail" },
|
|
15
|
+
{ "type": "text", "label": "Remaining", "scope": "detail" }
|
|
16
|
+
]
|
|
17
|
+
}
|