@hashimkarim/usagestat-linux-x64-gnu 2.1.0 → 2.2.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 (81) hide show
  1. package/native-manifest.json +197 -89
  2. package/package.json +19 -1
  3. package/plugins/abliteration/plugin.json +1 -1
  4. package/plugins/aerostack/plugin.json +1 -1
  5. package/plugins/aixy/plugin.json +1 -1
  6. package/plugins/atlascloud/plugin.json +1 -1
  7. package/plugins/bifrost/plugin.json +1 -1
  8. package/plugins/chatbox/plugin.json +1 -1
  9. package/plugins/claudeplatform/plugin.json +1 -1
  10. package/plugins/codex/plugin.js +11 -6
  11. package/plugins/codex/plugin.json +10 -1
  12. package/plugins/cosmic/plugin.json +1 -1
  13. package/plugins/cursor/plugin.js +18 -6
  14. package/plugins/devpass/plugin.json +1 -1
  15. package/plugins/exa/plugin.json +1 -1
  16. package/plugins/fal/plugin.json +1 -1
  17. package/plugins/flora/plugin.json +1 -1
  18. package/plugins/futureagi/plugin.json +1 -1
  19. package/plugins/gemini-apps/plugin.json +1 -1
  20. package/plugins/gitkraken/plugin.json +1 -1
  21. package/plugins/googledeveloper/LICENSE +21 -0
  22. package/plugins/googledeveloper/icon.svg +3 -0
  23. package/plugins/googledeveloper/plugin.js +204 -0
  24. package/plugins/googledeveloper/plugin.json +25 -0
  25. package/plugins/helmcode/plugin.js +80 -23
  26. package/plugins/helmcode/plugin.json +2 -2
  27. package/plugins/huggingface/plugin.json +1 -1
  28. package/plugins/hyper/plugin.json +1 -1
  29. package/plugins/jetbrains-ai-assistant/LICENSE +22 -0
  30. package/plugins/jetbrains-ai-assistant/plugin.js +502 -16
  31. package/plugins/jetbrains-ai-assistant/plugin.json +27 -4
  32. package/plugins/kimi/plugin.js +16 -1
  33. package/plugins/kimi/plugin.json +46 -6
  34. package/plugins/langdock/plugin.json +1 -1
  35. package/plugins/linkup/plugin.json +1 -1
  36. package/plugins/lithosai/plugin.json +1 -1
  37. package/plugins/llmman/plugin.json +1 -1
  38. package/plugins/munito/plugin.json +1 -1
  39. package/plugins/muse/plugin.json +1 -1
  40. package/plugins/museai/plugin.js +5 -3
  41. package/plugins/museai/plugin.json +2 -2
  42. package/plugins/nous/plugin.json +1 -1
  43. package/plugins/opencode-go/plugin.js +4 -3
  44. package/plugins/opendesign/LICENSE +21 -0
  45. package/plugins/opendesign/icon.svg +4 -0
  46. package/plugins/opendesign/plugin.js +108 -0
  47. package/plugins/opendesign/plugin.json +25 -0
  48. package/plugins/parallel/plugin.json +1 -1
  49. package/plugins/perplexity/LICENSE +22 -0
  50. package/plugins/perplexity/plugin.js +122 -0
  51. package/plugins/perplexity/plugin.json +36 -7
  52. package/plugins/qwencloud/plugin.json +1 -1
  53. package/plugins/raycast/plugin.json +1 -1
  54. package/plugins/replicate/plugin.json +1 -1
  55. package/plugins/sailresearch/plugin.json +1 -1
  56. package/plugins/siliconflow/LICENSE +21 -0
  57. package/plugins/siliconflow/icon.svg +3 -0
  58. package/plugins/siliconflow/plugin.js +87 -0
  59. package/plugins/siliconflow/plugin.json +32 -0
  60. package/plugins/sofya/plugin.json +1 -1
  61. package/plugins/soniox/plugin.json +1 -1
  62. package/plugins/synthetic/plugin.js +12 -0
  63. package/plugins/synthetic/plugin.json +51 -9
  64. package/plugins/tavily/plugin.json +1 -1
  65. package/plugins/tinyapi/plugin.json +1 -1
  66. package/plugins/traecn/plugin.js +10 -11
  67. package/plugins/traecn/plugin.json +2 -2
  68. package/plugins/tripo/LICENSE +21 -0
  69. package/plugins/tripo/icon.svg +4 -0
  70. package/plugins/tripo/plugin.js +60 -0
  71. package/plugins/tripo/plugin.json +24 -0
  72. package/plugins/typesafe/plugin.js +63 -112
  73. package/plugins/typesafe/plugin.json +2 -2
  74. package/plugins/v0/plugin.json +1 -1
  75. package/plugins/vercel/plugin.json +2 -2
  76. package/plugins/workbuddy/plugin.js +103 -17
  77. package/plugins/workbuddy/plugin.json +14 -2
  78. package/plugins/xapi/plugin.json +1 -1
  79. package/plugins/xkiro/plugin.json +1 -1
  80. package/usagestat +0 -0
  81. package/usagestatd +0 -0
@@ -1,9 +1,30 @@
1
+ // Adapted from barramee27/crossusage at 78b243b798abce1270d6fed775c79e65825b2c33; see LICENSE.
1
2
  (function () {
2
3
  var QUOTA_FILENAME = "AIAssistantQuotaManager2.xml"
4
+ var JUNIE_INSTANCES_DIR = "~/.junie/instances"
5
+ var JUNIE_SESSIONS_DIR = "~/.junie/sessions"
3
6
 
4
7
  // JetBrains stores AI quota in 1e-5 credit units; divide by this to get credits.
5
8
  var CREDIT_UNIT_SCALE = 100000
6
9
 
10
+ // Included credits on current JetBrains plans. The Aug 2025 currency model used
11
+ // 10 (Pro) and 35 (Ultimate); those caps clamp a live balance that is still
12
+ // inside the real tariff, and top-ups can sit above either number.
13
+ var AI_PRO_CREDITS = 20
14
+ var AI_ULTIMATE_CREDITS = 70
15
+ var UNKNOWN_PLAN_CREDIT_FLOOR = 10
16
+ // An undated IDE quota file has no mtime. Sessions older than this do not
17
+ // outrank it; a dated idea.log is compared by its own timestamp instead.
18
+ var SESSION_UNDATED_LIVE_MAX_AGE_MS = 48 * 60 * 60 * 1000
19
+
20
+ var LICENSE_NAMES = {
21
+ AIP: "JetBrains AI Pro",
22
+ AIPU: "JetBrains AI Ultimate",
23
+ AIF: "JetBrains AI Free",
24
+ TRIAL: "JetBrains AI Trial",
25
+ JUNP: "Junie EAP",
26
+ }
27
+
7
28
  var PRODUCT_PREFIXES = [
8
29
  "Aqua",
9
30
  "AndroidStudio",
@@ -30,6 +51,12 @@
30
51
  return [root]
31
52
  }
32
53
 
54
+ function platformLogDirs(ctx) {
55
+ if (ctx.app.platform === "macos" || ctx.app.platform === "darwin") return [ctx.host.fs.homeDir+"/Library/Logs/JetBrains"]
56
+ if (ctx.app.platform === "windows") return [ctx.host.fs.localAppDataPath("JetBrains")].filter(Boolean)
57
+ return ctx.host.fs.cacheDir ? [ctx.host.fs.cacheDir+"/JetBrains"] : []
58
+ }
59
+
33
60
  function isLikelyIdeDirName(name) {
34
61
  if (typeof name !== "string") return false
35
62
  var trimmed = name.trim()
@@ -56,7 +83,7 @@
56
83
 
57
84
  try {
58
85
  var entries = ctx.host.fs.listDir(path)
59
- return Array.isArray(entries) ? entries : []
86
+ return Array.isArray(entries) ? entries.slice().sort().reverse().slice(0,64) : []
60
87
  } catch (e) {
61
88
  ctx.host.log.warn("listDir failed for " + path + ": " + String(e))
62
89
  return []
@@ -89,6 +116,33 @@
89
116
  return paths
90
117
  }
91
118
 
119
+ function buildIdeaLogPaths(ctx) {
120
+ var bases = platformLogDirs(ctx)
121
+ var paths = []
122
+ var seen = Object.create(null)
123
+ for (var b = 0; b < bases.length; b += 1) {
124
+ var base = bases[b]
125
+ var entries = safeListDir(ctx, base)
126
+ for (var i = 0; i < entries.length; i += 1) {
127
+ var dirName = entries[i]
128
+ if (!isLikelyIdeDirName(dirName)) continue
129
+ var candidates = [
130
+ base + "/" + dirName + "/log/idea.log",
131
+ base + "/" + dirName + "/idea.log",
132
+ ]
133
+ for (var c = 0; c < candidates.length; c += 1) {
134
+ var logPath = candidates[c]
135
+ if (!ctx.host.fs.exists(logPath)) continue
136
+ if (!seen[logPath]) {
137
+ seen[logPath] = true
138
+ paths.push(logPath)
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return paths
144
+ }
145
+
92
146
  function decodeXmlEntities(text) {
93
147
  if (!text) return ""
94
148
  return String(text)
@@ -110,6 +164,7 @@
110
164
  }
111
165
 
112
166
  function toNumber(value) {
167
+ if (value === null || value === undefined || value === "") return null
113
168
  var n = Number(value)
114
169
  return Number.isFinite(n) ? n : null
115
170
  }
@@ -120,12 +175,30 @@
120
175
  return value
121
176
  }
122
177
 
178
+ function resolvePlanLabel(licenseName, licenseType) {
179
+ if (typeof licenseName === "string" && licenseName.trim()) {
180
+ return licenseName.trim()
181
+ }
182
+ if (typeof licenseType === "string" && licenseType.trim()) {
183
+ var code = licenseType.trim()
184
+ if (LICENSE_NAMES[code]) return LICENSE_NAMES[code]
185
+ return code
186
+ }
187
+ return null
188
+ }
189
+
123
190
  function normalizeQuota(quotaInfo) {
124
191
  if (!quotaInfo || typeof quotaInfo !== "object") return null
125
192
 
126
193
  var maximum = toNumber(quotaInfo.maximum)
194
+ if (maximum === null) maximum = toNumber(quotaInfo.creditsMaximum)
195
+
127
196
  var used = toNumber(quotaInfo.current)
197
+ if (used === null) used = toNumber(quotaInfo.creditsUsed)
198
+
128
199
  var remaining = toNumber(quotaInfo.available)
200
+ if (remaining === null) remaining = toNumber(quotaInfo.creditsAvailable)
201
+ if (remaining === null) remaining = toNumber(quotaInfo.balanceLeft)
129
202
 
130
203
  var tariff = quotaInfo.tariffQuota && typeof quotaInfo.tariffQuota === "object"
131
204
  ? quotaInfo.tariffQuota
@@ -158,6 +231,10 @@
158
231
  }
159
232
  }
160
233
 
234
+ if (used === null && maximum !== null && remaining !== null) {
235
+ used = maximum - remaining
236
+ }
237
+
161
238
  if (remaining === null && maximum !== null && used !== null) {
162
239
  remaining = maximum - used
163
240
  }
@@ -167,31 +244,304 @@
167
244
  used = clamp(used, 0, maximum)
168
245
  if (remaining !== null) remaining = clamp(remaining, 0, maximum)
169
246
 
247
+ var until = quotaInfo.until || null
248
+ if (!until && quotaInfo.nextRefillMillis) {
249
+ var refillMs = toNumber(quotaInfo.nextRefillMillis)
250
+ if (refillMs !== null && refillMs > 0) {
251
+ until = new Date(refillMs).toISOString()
252
+ }
253
+ }
254
+
170
255
  return {
171
256
  used: used,
172
257
  maximum: maximum,
173
258
  remaining: remaining,
174
- until: quotaInfo.until || null,
259
+ until: until,
260
+ plan: resolvePlanLabel(quotaInfo.licenseName, quotaInfo.licenseType),
175
261
  }
176
262
  }
177
263
 
178
264
  function readQuotaState(ctx, path) {
179
265
  if (!ctx.host.fs.exists(path)) return null
180
266
  try {
181
- var xml = ctx.host.fs.readText(path)
267
+ var xml = ctx.host.fs.readTextLimited(path, 2097152)
182
268
  var quotaInfo = parseOptionJson(ctx, xml, "quotaInfo")
183
269
  var nextRefill = parseOptionJson(ctx, xml, "nextRefill")
184
270
  var quota = normalizeQuota(quotaInfo)
185
271
  if (!quota) return null
186
- return { path: path, quota: quota, nextRefill: nextRefill }
272
+ return { path: path, quota: quota, nextRefill: nextRefill, tier: 5 }
187
273
  } catch (e) {
188
274
  ctx.host.log.warn("failed reading quota state " + path + ": " + String(e))
189
275
  return null
190
276
  }
191
277
  }
192
278
 
193
- // Handles only simple single-component durations (PTnH, PnD, PnW).
194
- // JetBrains currently uses values like PT720H or P30D.
279
+ // Tier 1: Live JetBrains IDE Local JSON-RPC Bridge (~/.junie/instances/*.json)
280
+ function probeLiveIdeRpc(ctx) {
281
+ if (!ctx.host.http || typeof ctx.host.http.request !== "function") return []
282
+ var entries = safeListDir(ctx, JUNIE_INSTANCES_DIR)
283
+ var states = []
284
+
285
+ for (var i = 0; i < entries.length; i += 1) {
286
+ var fileName = entries[i]
287
+ if (typeof fileName !== "string" || !/\.json$/i.test(fileName)) continue
288
+ var instancePath = JUNIE_INSTANCES_DIR + "/" + fileName
289
+ try {
290
+ var raw = ctx.host.fs.readTextLimited(instancePath, 2097152)
291
+ var instance = ctx.util.tryParseJson(raw)
292
+ if (!instance || typeof instance !== "object") continue
293
+ var port = toNumber(instance.port)
294
+ var rpcToken = typeof instance.rpc_token === "string" ? instance.rpc_token.trim() : ""
295
+ if (!port || port <= 0 || port > 65535 || !rpcToken) continue
296
+
297
+ var resp = ctx.host.http.request({
298
+ method: "POST",
299
+ url: "http://127.0.0.1:" + String(port) + "/rpc",
300
+ headers: {
301
+ "Content-Type": "application/json",
302
+ Authorization: rpcToken,
303
+ },
304
+ bodyText: JSON.stringify({
305
+ jsonrpc: "2.0",
306
+ id: 1,
307
+ method: "JunieSystemService.getQuotaInfo",
308
+ params: {},
309
+ }),
310
+ timeoutMs: 1500,
311
+ })
312
+
313
+ if (!resp || resp.status < 200 || resp.status >= 300 || !resp.bodyText) continue
314
+ var payload = ctx.util.tryParseJson(resp.bodyText)
315
+ if (!payload || !payload.result || payload.result.error) continue
316
+
317
+ var quota = normalizeQuota(payload.result)
318
+ if (!quota) continue
319
+
320
+ var nextRefill = null
321
+ if (payload.result.nextRefillMillis) {
322
+ var refillMs = toNumber(payload.result.nextRefillMillis)
323
+ if (refillMs !== null && refillMs > 0) {
324
+ nextRefill = { next: new Date(refillMs).toISOString() }
325
+ }
326
+ }
327
+
328
+ states.push({
329
+ path: "rpc://127.0.0.1:" + String(port) + " (" + fileName + ")",
330
+ quota: quota,
331
+ nextRefill: nextRefill,
332
+ tier: 1,
333
+ })
334
+ } catch (e) {
335
+ ctx.host.log.warn("IDE RPC probe skipped for " + instancePath + ": " + String(e))
336
+ }
337
+ }
338
+
339
+ return states
340
+ }
341
+
342
+ function coerceEpochMs(ctx, value) {
343
+ var n = toNumber(value)
344
+ if (n !== null) {
345
+ if (n <= 0) return null
346
+ return n < 1000000000000 ? n * 1000 : n
347
+ }
348
+ if (typeof value === "string" && value.trim()) {
349
+ return ctx.util.parseDateMs(value.trim())
350
+ }
351
+ return null
352
+ }
353
+
354
+ function readObservedAtMs(ctx, value) {
355
+ if (!value || typeof value !== "object") return null
356
+ var keys = ["timestampMs", "timestamp", "timeMs", "createdAt", "updatedAt"]
357
+ for (var i = 0; i < keys.length; i += 1) {
358
+ var ms = coerceEpochMs(ctx, value[keys[i]])
359
+ if (ms !== null) return ms
360
+ }
361
+ return null
362
+ }
363
+
364
+ function eventObservedAtMs(ctx, parsed) {
365
+ var direct = readObservedAtMs(ctx, parsed)
366
+ if (direct !== null) return direct
367
+ if (!parsed || typeof parsed !== "object") return null
368
+ var fromEvent = readObservedAtMs(ctx, parsed.event)
369
+ if (fromEvent !== null) return fromEvent
370
+ return readObservedAtMs(ctx, parsed.completion)
371
+ }
372
+
373
+ function sessionIdObservedAtMs(sessionId) {
374
+ if (typeof sessionId !== "string") return null
375
+ var match = sessionId.match(/^session-(\d{2})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})(?:-|$)/)
376
+ if (!match) return null
377
+ var ms = Date.parse(
378
+ "20" +
379
+ match[1] +
380
+ "-" +
381
+ match[2] +
382
+ "-" +
383
+ match[3] +
384
+ "T" +
385
+ match[4] +
386
+ ":" +
387
+ match[5] +
388
+ ":" +
389
+ match[6] +
390
+ "Z"
391
+ )
392
+ return Number.isFinite(ms) ? ms : null
393
+ }
394
+
395
+ function readJunieSessionIndexTimes(ctx) {
396
+ var times = Object.create(null)
397
+ var indexPath = JUNIE_SESSIONS_DIR + "/index.jsonl"
398
+ if (!ctx.host.fs.exists(indexPath)) return times
399
+ try {
400
+ var text = ctx.host.fs.readTextLimited(indexPath, 2097152)
401
+ if (!text) return times
402
+ var lines = text.split(/\r?\n/)
403
+ for (var i = 0; i < lines.length; i += 1) {
404
+ var line = lines[i].trim()
405
+ if (!line) continue
406
+ var row = ctx.util.tryParseJson(line)
407
+ if (!row || typeof row.sessionId !== "string") continue
408
+ var at = coerceEpochMs(ctx, row.updatedAt)
409
+ if (at === null) at = coerceEpochMs(ctx, row.createdAt)
410
+ if (at !== null) times[row.sessionId] = at
411
+ }
412
+ } catch (e) {
413
+ ctx.host.log.warn("failed reading junie session index: " + String(e))
414
+ }
415
+ return times
416
+ }
417
+
418
+ function logLineObservedAtMs(line) {
419
+ if (typeof line !== "string") return null
420
+ var match = line.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:,(\d{1,3}))?/)
421
+ if (!match) return null
422
+ var frac = match[3] || "000"
423
+ if (frac.length > 3) frac = frac.slice(0, 3)
424
+ while (frac.length < 3) frac = frac + "0"
425
+ // idea.log stamps are local wall time (ZoneId.systemDefault) and carry no offset.
426
+ // Date.parse treats a date-time without Z as local. Appending Z would read that
427
+ // wall clock as UTC and mis-rank it against real Unix session timestamps.
428
+ var ms = Date.parse(match[1] + "T" + match[2] + "." + frac)
429
+ return Number.isFinite(ms) ? ms : null
430
+ }
431
+
432
+ // Tier 3: Junie CLI Session Logs (~/.junie/sessions/*/events.jsonl)
433
+ // Each session keeps the observation time of its newest quota event so a
434
+ // leftover folder cannot outrank a later idea.log or XML snapshot.
435
+ function probeJunieSessions(ctx) {
436
+ var sessionDirs = safeListDir(ctx, JUNIE_SESSIONS_DIR)
437
+ var indexTimes = readJunieSessionIndexTimes(ctx)
438
+ var states = []
439
+
440
+ for (var i = 0; i < sessionDirs.length; i += 1) {
441
+ var sessionId = sessionDirs[i]
442
+ var eventsPath = JUNIE_SESSIONS_DIR + "/" + sessionId + "/events.jsonl"
443
+ if (!ctx.host.fs.exists(eventsPath)) continue
444
+ try {
445
+ var text = ctx.host.fs.readTextLimited(eventsPath, 2097152)
446
+ if (!text) continue
447
+ var lines = text.split(/\r?\n/)
448
+ for (var l = lines.length - 1; l >= 0; l -= 1) {
449
+ var line = lines[l].trim()
450
+ if (!line || line.indexOf("quota") === -1) continue
451
+ var parsed = ctx.util.tryParseJson(line)
452
+ if (!parsed || typeof parsed !== "object") continue
453
+ var candidate =
454
+ (parsed.completion && parsed.completion.quota) ||
455
+ parsed.quotaInfo ||
456
+ parsed.quota ||
457
+ null
458
+ if (!candidate) continue
459
+ var quota = normalizeQuota(candidate)
460
+ if (quota) {
461
+ var observedAt = eventObservedAtMs(ctx, parsed)
462
+ if (observedAt === null && typeof indexTimes[sessionId] === "number") {
463
+ observedAt = indexTimes[sessionId]
464
+ }
465
+ if (observedAt === null) observedAt = sessionIdObservedAtMs(sessionId)
466
+ states.push({
467
+ path: eventsPath,
468
+ quota: quota,
469
+ nextRefill: candidate.nextRefill || null,
470
+ tier: 3,
471
+ observedAtMs: observedAt,
472
+ })
473
+ break
474
+ }
475
+ }
476
+ } catch (e) {
477
+ ctx.host.log.warn("failed reading junie session " + eventsPath + ": " + String(e))
478
+ }
479
+ }
480
+
481
+ return states
482
+ }
483
+
484
+ // Tier 4: IDE Application Logs (idea.log -> QuotaManager2Impl)
485
+ function extractLastLogQuotaState(ctx, logPath) {
486
+ try {
487
+ var text = ctx.host.fs.readTextLimited(logPath, 2097152)
488
+ if (!text || text.indexOf("QuotaManager2") === -1) return null
489
+ var lines = text.split(/\r?\n/)
490
+ var latestQuota = null
491
+ var latestRefill = null
492
+ var latestObservedAt = null
493
+
494
+ for (var i = lines.length - 1; i >= 0; i -= 1) {
495
+ var line = lines[i]
496
+ if (!latestQuota && line.indexOf("New quota info") !== -1) {
497
+ var braceIdx = line.indexOf("{")
498
+ if (braceIdx !== -1) {
499
+ var jsonCandidate = line.slice(braceIdx).trim()
500
+ var parsed = ctx.util.tryParseJson(jsonCandidate)
501
+ if (parsed) {
502
+ latestQuota = normalizeQuota(parsed)
503
+ if (latestQuota) latestObservedAt = logLineObservedAtMs(line)
504
+ }
505
+ }
506
+ }
507
+ if (!latestRefill && line.indexOf("New next refill") !== -1) {
508
+ var refillBraceIdx = line.indexOf("{")
509
+ if (refillBraceIdx !== -1) {
510
+ latestRefill = ctx.util.tryParseJson(line.slice(refillBraceIdx).trim())
511
+ }
512
+ }
513
+ if (latestQuota && latestRefill) break
514
+ }
515
+
516
+ if (!latestQuota) return null
517
+ return {
518
+ path: logPath,
519
+ quota: latestQuota,
520
+ nextRefill: latestRefill,
521
+ tier: 4,
522
+ observedAtMs: latestObservedAt,
523
+ }
524
+ } catch (e) {
525
+ ctx.host.log.warn("failed reading idea.log " + logPath + ": " + String(e))
526
+ return null
527
+ }
528
+ }
529
+
530
+ function probeIdeaLogs(ctx) {
531
+ var logPaths = buildIdeaLogPaths(ctx)
532
+ var states = []
533
+ for (var i = 0; i < logPaths.length; i += 1) {
534
+ var state = extractLastLogQuotaState(ctx, logPaths[i])
535
+ if (state) states.push(state)
536
+ }
537
+ return states
538
+ }
539
+
540
+ // Tier 2: Junie Cloud API (~/.junie/secure_credentials.json or OS keychain -> Ingrazzio /auth/test)
541
+ function probeNowMs(ctx) {
542
+ return Date.parse(ctx.nowIso) || Date.now()
543
+ }
544
+
195
545
  function parseIsoDurationMs(value) {
196
546
  if (typeof value !== "string" || !value) return null
197
547
 
@@ -207,6 +557,100 @@
207
557
  return null
208
558
  }
209
559
 
560
+ function filterSessionStates(ctx, sessionStates, liveStates) {
561
+ if (!liveStates || liveStates.length === 0) return sessionStates
562
+ var newestLiveAt = null
563
+ for (var i = 0; i < liveStates.length; i += 1) {
564
+ var liveAt = liveStates[i].observedAtMs
565
+ if (typeof liveAt === "number" && (newestLiveAt === null || liveAt > newestLiveAt)) {
566
+ newestLiveAt = liveAt
567
+ }
568
+ }
569
+ var nowMs = ctx.nowIso ? ctx.util.parseDateMs(ctx.nowIso) : null
570
+ if (nowMs === null) nowMs = Date.now()
571
+ var kept = []
572
+ for (var s = 0; s < sessionStates.length; s += 1) {
573
+ var session = sessionStates[s]
574
+ var sessionAt = session.observedAtMs
575
+ if (typeof sessionAt !== "number") continue
576
+ if (newestLiveAt !== null) {
577
+ if (sessionAt >= newestLiveAt) kept.push(session)
578
+ continue
579
+ }
580
+ if (nowMs - sessionAt <= SESSION_UNDATED_LIVE_MAX_AGE_MS) kept.push(session)
581
+ }
582
+ return kept
583
+ }
584
+
585
+ function localSourceRank(tier) {
586
+ var rank = tier || 5
587
+ // Session logs share `until` with idea.log and XML. That timestamp is the
588
+ // billing-window end, so a lower tier number must not win the tie.
589
+ if (rank === 3) return 6
590
+ return rank
591
+ }
592
+
593
+ function pickBestState(ctx, states) {
594
+ var best = null
595
+ var bestMs = -Infinity
596
+
597
+ for (var i = 0; i < states.length; i += 1) {
598
+ var state = states[i]
599
+ var untilMs = null
600
+
601
+ if (state.quota.until) {
602
+ untilMs = ctx.util.parseDateMs(state.quota.until)
603
+ }
604
+ if (untilMs === null && state.nextRefill && state.nextRefill.next) {
605
+ untilMs = ctx.util.parseDateMs(state.nextRefill.next)
606
+ }
607
+ if (untilMs === null) untilMs = -Infinity
608
+
609
+ if (!best || untilMs > bestMs) {
610
+ best = state
611
+ bestMs = untilMs
612
+ continue
613
+ }
614
+
615
+ if (untilMs === bestMs) {
616
+ var stateAt = typeof state.observedAtMs === "number" ? state.observedAtMs : null
617
+ var bestAt = typeof best.observedAtMs === "number" ? best.observedAtMs : null
618
+ if (stateAt !== null && bestAt !== null && stateAt !== bestAt) {
619
+ if (stateAt > bestAt) {
620
+ best = state
621
+ continue
622
+ }
623
+ continue
624
+ }
625
+
626
+ var stateRank = localSourceRank(state.tier)
627
+ var bestRank = localSourceRank(best.tier)
628
+ if (stateRank < bestRank) {
629
+ best = state
630
+ continue
631
+ }
632
+ if (stateRank > bestRank) {
633
+ continue
634
+ }
635
+
636
+ var currentRatio =
637
+ state.quota.maximum > 0 ? state.quota.used / state.quota.maximum : 0
638
+ var bestRatio =
639
+ best.quota.maximum > 0 ? best.quota.used / best.quota.maximum : 0
640
+ if (currentRatio > bestRatio) {
641
+ best = state
642
+ continue
643
+ }
644
+ if (currentRatio === bestRatio && state.quota.used > best.quota.used) {
645
+ best = state
646
+ continue
647
+ }
648
+ }
649
+ }
650
+
651
+ return best
652
+ }
653
+
210
654
  function formatDecimal(value, places) {
211
655
  if (!Number.isFinite(value)) return null
212
656
  var factor = Math.pow(10, places)
@@ -233,24 +677,64 @@
233
677
  }
234
678
 
235
679
  function probe(ctx) {
236
- var paths = buildQuotaPaths(ctx)
237
- var states = []
680
+ if (ctx.provider?.instanceId && ctx.provider.instanceId !== "jetbrains-ai-assistant")
681
+ throw "JetBrains local observations are available only for the default account."
238
682
 
683
+ // 1. Check live running JetBrains IDEs via Junie local JSON-RPC bridge first
684
+ var settings = ctx.provider?.settings || {}
685
+ var paths = buildQuotaPaths(ctx)
686
+ if (paths.length > 1 && !settings.configDir)
687
+ throw {code:"failed",message:"Multiple JetBrains IDE quota profiles found. Select settings.configDir to choose one installation."}
688
+ // Junie observations may span IDEs. Require an explicit opt-in, and never
689
+ // borrow them when an individual configuration directory was selected.
690
+ var live = settings.liveQuota === true && !settings.configDir
691
+ var rpcStates = live ? probeLiveIdeRpc(ctx) : []
692
+
693
+ // 2. Gather local XML, session, and idea.log states
694
+ var xmlStates = []
239
695
  for (var i = 0; i < paths.length; i += 1) {
240
- var state = readQuotaState(ctx, paths[i])
241
- if (state) states.push(state)
696
+ var xmlState = readQuotaState(ctx, paths[i])
697
+ if (xmlState) xmlStates.push(xmlState)
698
+ }
699
+
700
+ var sessionStates = live ? probeJunieSessions(ctx) : []
701
+ var logStates = live ? probeIdeaLogs(ctx) : []
702
+ var liveStates = logStates.concat(xmlStates)
703
+ // Drop session snapshots that are older than the live IDE files. A shared
704
+ // billing-window `until` would otherwise let a leftover events.jsonl win,
705
+ var sessionCandidates = filterSessionStates(ctx, sessionStates, liveStates)
706
+
707
+ var localFallbackBest = pickBestState(ctx, sessionCandidates.concat(liveStates))
708
+
709
+ var chosen = null
710
+ if (rpcStates.length > 0) {
711
+ chosen = pickBestState(ctx, rpcStates)
712
+ // Preserve tariff duration metadata from XML if RPC response only has nextRefillMillis
713
+ if (
714
+ chosen &&
715
+ localFallbackBest &&
716
+ localFallbackBest.nextRefill &&
717
+ localFallbackBest.nextRefill.tariff &&
718
+ (!chosen.nextRefill || !chosen.nextRefill.tariff)
719
+ ) {
720
+ chosen.nextRefill = {
721
+ next:
722
+ (chosen.nextRefill && chosen.nextRefill.next) ||
723
+ localFallbackBest.nextRefill.next ||
724
+ null,
725
+ tariff: localFallbackBest.nextRefill.tariff,
726
+ }
727
+ }
728
+ } else {
729
+ chosen = localFallbackBest
242
730
  }
243
731
 
244
- if (states.length === 0) {
732
+ if (!chosen) {
245
733
  throw paths.length > 0
246
734
  ? "JetBrains AI Assistant quota data unavailable. Open AI Assistant once and try again."
247
735
  : "JetBrains AI Assistant not detected. Open a JetBrains IDE with AI Assistant enabled."
248
736
  }
249
737
 
250
- if (states.length > 1) {
251
- throw {code: "failed", message: "Multiple JetBrains IDE quota profiles found. Select settings.configDir to choose one installation."}
252
- }
253
- var chosen = states[0]
254
738
  var quota = chosen.quota
255
739
  var scale = detectDisplayScale(quota, chosen.nextRefill)
256
740
  var usedPercent = (quota.used / quota.maximum) * 100
@@ -306,7 +790,9 @@
306
790
 
307
791
  ctx.host.log.info("quota loaded from " + chosen.path)
308
792
 
309
- return { lines: lines }
793
+ var result = { lines: lines }
794
+ if (quota.plan) result.plan = quota.plan
795
+ return result
310
796
  }
311
797
 
312
798
  globalThis.__openusage_plugin = { id: "jetbrains-ai-assistant", probe: probe }
@@ -6,11 +6,34 @@
6
6
  "entry": "plugin.js",
7
7
  "icon": "icon.svg",
8
8
  "brandColor": "#7d5fe6",
9
- "supportedModes": ["local"],
9
+ "supportedModes": [
10
+ "local"
11
+ ],
10
12
  "autoMode": "local",
11
13
  "lines": [
12
- { "type": "progress", "label": "Quota", "scope": "overview", "primaryOrder": 1 },
13
- { "type": "text", "label": "Used", "scope": "detail" },
14
- { "type": "text", "label": "Remaining", "scope": "detail" }
14
+ {
15
+ "type": "progress",
16
+ "label": "Quota",
17
+ "scope": "overview",
18
+ "primaryOrder": 1
19
+ },
20
+ {
21
+ "type": "text",
22
+ "label": "Used",
23
+ "scope": "detail"
24
+ },
25
+ {
26
+ "type": "text",
27
+ "label": "Remaining",
28
+ "scope": "detail"
29
+ }
30
+ ],
31
+ "setupFields": [
32
+ {
33
+ "key": "liveQuota",
34
+ "type": "boolean",
35
+ "title": "Read local Junie observations",
36
+ "description": "Read running IDE quotas, Junie session observations and IDE logs for the default account. Disabled when configDir selects one IDE; no cloud token refresh."
37
+ }
15
38
  ]
16
39
  }