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 @@
1
+ <svg width="100" height="100" viewBox="0 0 67 65" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M47.75 11.15a.867.867 0 0 1-.671-.806.84.84 0 0 1 .067-.362c1.688-4.007 2.433-7.213 1.23-8.555-3.183-3.56-15.952 3.52-20.024 5.919a.9.9 0 0 1-1.273-.41c-1.711-3.998-3.51-6.78-5.334-6.9-4.833-.323-8.73 13.49-9.87 17.992a.85.85 0 0 1-.459.563.9.9 0 0 1-.737.027c-4.109-1.647-7.398-2.373-8.773-1.2-3.651 3.104 3.609 15.557 6.068 19.528a.85.85 0 0 1-.11 1.031.9.9 0 0 1-.31.21C3.455 39.856.604 41.61.478 43.389c-.329 4.713 13.834 8.513 18.452 9.625q.186.046.337.163a.87.87 0 0 1 .332.642.84.84 0 0 1-.067.362c-1.688 4.007-2.433 7.214-1.23 8.555 3.183 3.561 15.954-3.519 20.025-5.917a.9.9 0 0 1 1.058.107.9.9 0 0 1 .215.302c1.711 3.997 3.509 6.779 5.334 6.9 4.833.322 8.73-13.49 9.868-17.993a.85.85 0 0 1 .168-.33.88.88 0 0 1 .659-.324.9.9 0 0 1 .371.066c4.109 1.647 7.397 2.372 8.773 1.2 3.651-3.105-3.61-15.559-6.07-19.53a.85.85 0 0 1 .111-1.03.9.9 0 0 1 .31-.21c4.1-1.67 6.952-3.424 7.075-5.203.331-4.713-13.833-8.513-18.45-9.623m-5.546-4.518c.93 1.624-3.858 12.446-7.42 20.015a.7.7 0 0 1-.28.303.71.71 0 0 1-.796-.059.7.7 0 0 1-.23-.341c-1.439-4.921-3.082-10.704-4.841-15.612a.84.84 0 0 1 .01-.594.87.87 0 0 1 .401-.446c4.392-2.34 11.908-5.446 13.156-3.266m-21.048 1.34c1.833.507 6.294 11.46 9.264 19.268a.67.67 0 0 1-.2.754.71.71 0 0 1-.794.08c-4.589-2.485-9.94-5.444-14.743-7.702a.87.87 0 0 1-.422-.427.84.84 0 0 1-.04-.591c1.414-4.679 4.471-12.063 6.935-11.383M7.243 23.433c1.664-.906 12.762 3.763 20.522 7.235.13.058.239.154.311.274a.67.67 0 0 1-.06.776.7.7 0 0 1-.35.225c-5.045 1.403-10.976 3.006-16.01 4.721a.9.9 0 0 1-.607-.01.88.88 0 0 1-.456-.391c-2.395-4.284-5.586-11.613-3.35-12.83M8.617 43.96c.519-1.788 11.752-6.14 19.758-9.035a.72.72 0 0 1 .773.195.67.67 0 0 1 .081.774c-2.548 4.475-5.582 9.694-7.898 14.377a.87.87 0 0 1-.437.413.9.9 0 0 1-.607.039c-4.797-1.37-12.37-4.36-11.67-6.763m15.855 13.568c-.93-1.623 3.859-12.446 7.42-20.014a.7.7 0 0 1 .28-.303.715.715 0 0 1 .796.059.7.7 0 0 1 .23.34c1.439 4.92 3.083 10.705 4.841 15.613a.84.84 0 0 1-.01.593.87.87 0 0 1-.402.445c-4.391 2.335-11.908 5.447-13.15 3.267zm21.049-1.34c-1.836-.506-6.297-11.461-9.266-19.269a.67.67 0 0 1 .2-.755.71.71 0 0 1 .795-.078c4.587 2.484 9.94 5.445 14.742 7.703.189.088.339.24.423.426a.84.84 0 0 1 .039.592c-1.413 4.686-4.47 12.063-6.933 11.381m13.912-15.462c-1.665.907-12.762-3.763-20.523-7.236a.7.7 0 0 1-.311-.273.67.67 0 0 1 .06-.777.7.7 0 0 1 .35-.225c5.046-1.402 10.975-3.005 16.009-4.72a.9.9 0 0 1 .609.01.88.88 0 0 1 .457.392c2.393 4.282 5.584 11.613 3.349 12.829M58.06 20.2c-.521 1.79-11.753 6.14-19.759 9.036a.72.72 0 0 1-.774-.195.67.67 0 0 1-.08-.776c2.547-4.474 5.581-9.694 7.897-14.377a.87.87 0 0 1 .437-.412.9.9 0 0 1 .607-.038c4.797 1.377 12.37 4.359 11.672 6.762"></path></svg>
@@ -0,0 +1,407 @@
1
+ (function () {
2
+ const AUTH_PATHS = ["~/.factory/auth.json", "~/.factory/auth.encrypted"]
3
+ const KEYCHAIN_SERVICES = ["Factory Token", "Factory token", "Factory Auth", "Droid Auth"]
4
+ const WORKOS_CLIENT_ID = "client_01HNM792M5G5G1A2THWPXKFMXB"
5
+ const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate"
6
+ const USAGE_URL = "https://api.factory.ai/api/organization/subscription/usage"
7
+ const TOKEN_REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1000 // 24 hours before expiry
8
+
9
+ function decodeHexUtf8(hex) {
10
+ try {
11
+ const bytes = []
12
+ for (let i = 0; i < hex.length; i += 2) {
13
+ bytes.push(parseInt(hex.slice(i, i + 2), 16))
14
+ }
15
+
16
+ if (typeof TextDecoder !== "undefined") {
17
+ try {
18
+ return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes))
19
+ } catch {}
20
+ }
21
+
22
+ let escaped = ""
23
+ for (const b of bytes) {
24
+ const h = b.toString(16)
25
+ escaped += "%" + (h.length === 1 ? "0" + h : h)
26
+ }
27
+ return decodeURIComponent(escaped)
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ function tryParseAuthJson(ctx, text) {
34
+ if (!text) return null
35
+ const parsed = ctx.util.tryParseJson(text)
36
+ if (parsed !== null) return parsed
37
+
38
+ // Some keychain payloads can be returned as hex-encoded UTF-8 bytes.
39
+ let hex = String(text).trim()
40
+ if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
41
+ if (!hex || hex.length % 2 !== 0) return null
42
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null
43
+
44
+ const decoded = decodeHexUtf8(hex)
45
+ if (!decoded) return null
46
+ return ctx.util.tryParseJson(decoded)
47
+ }
48
+
49
+ function looksLikeJwt(value) {
50
+ return /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(value)
51
+ }
52
+
53
+ function normalizeAuthPayload(raw, opts) {
54
+ const allowPartial = Boolean(opts && opts.allowPartial)
55
+ if (!raw || typeof raw !== "object") return null
56
+
57
+ const accessToken =
58
+ raw.access_token ||
59
+ raw.accessToken ||
60
+ (raw.tokens && (raw.tokens.access_token || raw.tokens.accessToken))
61
+
62
+ const refreshToken =
63
+ raw.refresh_token ||
64
+ raw.refreshToken ||
65
+ (raw.tokens && (raw.tokens.refresh_token || raw.tokens.refreshToken))
66
+
67
+ const hasAccess = typeof accessToken === "string" && accessToken
68
+ const hasRefresh = typeof refreshToken === "string" && refreshToken
69
+ if (!hasAccess && !(allowPartial && hasRefresh)) return null
70
+
71
+ return {
72
+ access_token: hasAccess ? accessToken : null,
73
+ refresh_token: hasRefresh ? refreshToken : null,
74
+ }
75
+ }
76
+
77
+ function parseAuthPayload(ctx, rawText, opts) {
78
+ const parsed = tryParseAuthJson(ctx, rawText)
79
+ const normalized = normalizeAuthPayload(parsed, opts)
80
+ if (normalized) return normalized
81
+
82
+ if (typeof parsed === "string" && looksLikeJwt(parsed)) {
83
+ return { access_token: parsed, refresh_token: null }
84
+ }
85
+
86
+ const direct = String(rawText || "").trim()
87
+ if (looksLikeJwt(direct)) {
88
+ return { access_token: direct, refresh_token: null }
89
+ }
90
+
91
+ return null
92
+ }
93
+
94
+ function loadAuthFromFiles(ctx) {
95
+ for (const authPath of AUTH_PATHS) {
96
+ if (!ctx.host.fs.exists(authPath)) continue
97
+
98
+ try {
99
+ const text = ctx.host.fs.readText(authPath)
100
+ const auth = parseAuthPayload(ctx, text, { allowPartial: true })
101
+ if (!auth) {
102
+ ctx.host.log.warn("auth file exists but has no valid auth payload: " + authPath)
103
+ continue
104
+ }
105
+ ctx.host.log.info("auth loaded from file: " + authPath)
106
+ return { auth, source: "file", authPath, keychainService: null }
107
+ } catch (e) {
108
+ ctx.host.log.warn("auth file read failed: " + String(e))
109
+ }
110
+ }
111
+
112
+ return null
113
+ }
114
+
115
+ function loadAuthFromKeychain(ctx) {
116
+ if (!ctx.host.keychain || typeof ctx.host.keychain.readGenericPassword !== "function") {
117
+ return null
118
+ }
119
+
120
+ for (const service of KEYCHAIN_SERVICES) {
121
+ try {
122
+ const value = ctx.host.keychain.readGenericPassword(service)
123
+ if (!value) continue
124
+
125
+ const auth = parseAuthPayload(ctx, value)
126
+ if (!auth) {
127
+ ctx.host.log.warn("keychain has data but no valid auth payload: " + service)
128
+ continue
129
+ }
130
+
131
+ ctx.host.log.info("auth loaded from keychain: " + service)
132
+ return { auth, source: "keychain", authPath: null, keychainService: service }
133
+ } catch (e) {
134
+ ctx.host.log.info("keychain read failed (may not exist): " + String(e))
135
+ }
136
+ }
137
+
138
+ return null
139
+ }
140
+
141
+ function loadAuth(ctx) {
142
+ const fileAuth = loadAuthFromFiles(ctx)
143
+ if (fileAuth) return fileAuth
144
+
145
+ const keychainAuth = loadAuthFromKeychain(ctx)
146
+ if (keychainAuth) return keychainAuth
147
+
148
+ for (const authPath of AUTH_PATHS) {
149
+ if (!ctx.host.fs.exists(authPath)) {
150
+ ctx.host.log.warn("auth file not found: " + authPath)
151
+ }
152
+ }
153
+
154
+ return null
155
+ }
156
+
157
+ function saveAuth(ctx, authState) {
158
+ const auth = authState && authState.auth ? authState.auth : null
159
+ if (!auth) return false
160
+
161
+ try {
162
+ if (authState.source === "file" && authState.authPath) {
163
+ ctx.host.fs.writeText(authState.authPath, JSON.stringify(auth, null, 2))
164
+ ctx.host.log.info("auth file updated: " + authState.authPath)
165
+ return true
166
+ }
167
+
168
+ if (
169
+ authState.source === "keychain" &&
170
+ authState.keychainService &&
171
+ ctx.host.keychain &&
172
+ typeof ctx.host.keychain.writeGenericPassword === "function"
173
+ ) {
174
+ ctx.host.keychain.writeGenericPassword(authState.keychainService, JSON.stringify(auth))
175
+ ctx.host.log.info("auth keychain item updated: " + authState.keychainService)
176
+ return true
177
+ }
178
+
179
+ ctx.host.log.warn("auth persistence skipped: unsupported source")
180
+ return false
181
+ } catch (e) {
182
+ ctx.host.log.warn("failed to save auth: " + String(e))
183
+ return false
184
+ }
185
+ }
186
+
187
+ function needsRefresh(ctx, accessToken, nowMs) {
188
+ const payload = ctx.jwt.decodePayload(accessToken)
189
+ const expiresAtMs = payload && typeof payload.exp === "number" ? payload.exp * 1000 : null
190
+ return ctx.util.needsRefreshByExpiry({
191
+ nowMs,
192
+ expiresAtMs,
193
+ bufferMs: TOKEN_REFRESH_THRESHOLD_MS,
194
+ })
195
+ }
196
+
197
+ function refreshToken(ctx, authState) {
198
+ const auth = authState.auth
199
+ if (!auth.refresh_token) {
200
+ ctx.host.log.warn("refresh skipped: no refresh token")
201
+ return null
202
+ }
203
+
204
+ ctx.host.log.info("attempting token refresh via WorkOS")
205
+ try {
206
+ const resp = ctx.util.request({
207
+ method: "POST",
208
+ url: WORKOS_AUTH_URL,
209
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
210
+ bodyText:
211
+ "grant_type=refresh_token" +
212
+ "&refresh_token=" + encodeURIComponent(auth.refresh_token) +
213
+ "&client_id=" + encodeURIComponent(WORKOS_CLIENT_ID),
214
+ timeoutMs: 15000,
215
+ })
216
+
217
+ if (resp.status === 400 || resp.status === 401) {
218
+ ctx.host.log.error("refresh failed: status=" + resp.status)
219
+ throw "Session expired. Run `droid` to log in again."
220
+ }
221
+ if (resp.status < 200 || resp.status >= 300) {
222
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
223
+ return null
224
+ }
225
+
226
+ const body = ctx.util.tryParseJson(resp.bodyText)
227
+ if (!body) {
228
+ ctx.host.log.warn("refresh response not valid JSON")
229
+ return null
230
+ }
231
+ const newAccessToken = body.access_token
232
+ if (!newAccessToken) {
233
+ ctx.host.log.warn("refresh response missing access_token")
234
+ return null
235
+ }
236
+
237
+ // Update auth object with new tokens
238
+ auth.access_token = newAccessToken
239
+ if (body.refresh_token) {
240
+ auth.refresh_token = body.refresh_token
241
+ }
242
+
243
+ // Save updated auth
244
+ saveAuth(ctx, authState)
245
+ ctx.host.log.info("refresh succeeded")
246
+
247
+ return newAccessToken
248
+ } catch (e) {
249
+ if (typeof e === "string") throw e
250
+ ctx.host.log.error("refresh exception: " + String(e))
251
+ return null
252
+ }
253
+ }
254
+
255
+ function fetchUsage(ctx, accessToken) {
256
+ return ctx.util.request({
257
+ method: "POST",
258
+ url: USAGE_URL,
259
+ headers: {
260
+ Authorization: "Bearer " + accessToken,
261
+ "Content-Type": "application/json",
262
+ Accept: "application/json",
263
+ "User-Agent": "OpenUsage",
264
+ },
265
+ bodyText: JSON.stringify({ useCache: true }),
266
+ timeoutMs: 10000,
267
+ })
268
+ }
269
+
270
+ function probe(ctx) {
271
+ const authState = loadAuth(ctx)
272
+ if (!authState) {
273
+ ctx.host.log.error("probe failed: not logged in")
274
+ throw "Not logged in. Run `droid` to authenticate."
275
+ }
276
+
277
+ const auth = authState.auth
278
+ if (!auth.access_token) {
279
+ ctx.host.log.error("probe failed: no access_token in auth data")
280
+ throw "Invalid auth file. Run `droid` to authenticate."
281
+ }
282
+
283
+ let accessToken = auth.access_token
284
+
285
+ // Check if token needs refresh
286
+ const nowMs = Date.now()
287
+ if (needsRefresh(ctx, accessToken, nowMs)) {
288
+ ctx.host.log.info("token near expiry, refreshing")
289
+ const refreshed = refreshToken(ctx, authState)
290
+ if (refreshed) {
291
+ accessToken = refreshed
292
+ } else {
293
+ ctx.host.log.warn("proactive refresh failed, trying with existing token")
294
+ }
295
+ }
296
+
297
+ let resp
298
+ let didRefresh = false
299
+ try {
300
+ resp = ctx.util.retryOnceOnAuth({
301
+ request: (token) => {
302
+ try {
303
+ return fetchUsage(ctx, token || accessToken)
304
+ } catch (e) {
305
+ ctx.host.log.error("usage request exception: " + String(e))
306
+ if (didRefresh) {
307
+ throw "Usage request failed after refresh. Try again."
308
+ }
309
+ throw "Usage request failed. Check your connection."
310
+ }
311
+ },
312
+ refresh: () => {
313
+ ctx.host.log.info("usage returned 401, attempting refresh")
314
+ didRefresh = true
315
+ return refreshToken(ctx, authState)
316
+ },
317
+ })
318
+ } catch (e) {
319
+ if (typeof e === "string") throw e
320
+ ctx.host.log.error("usage request failed: " + String(e))
321
+ throw "Usage request failed. Check your connection."
322
+ }
323
+
324
+ if (ctx.util.isAuthStatus(resp.status)) {
325
+ ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
326
+ throw "Token expired. Run `droid` to log in again."
327
+ }
328
+
329
+ if (resp.status < 200 || resp.status >= 300) {
330
+ ctx.host.log.error("usage returned error: status=" + resp.status)
331
+ throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
332
+ }
333
+
334
+ ctx.host.log.info("usage fetch succeeded")
335
+
336
+ const data = ctx.util.tryParseJson(resp.bodyText)
337
+ if (data === null) {
338
+ throw "Usage response invalid. Try again later."
339
+ }
340
+
341
+ const usage = data.usage
342
+ if (!usage) {
343
+ throw "Usage response missing data. Try again later."
344
+ }
345
+
346
+ const lines = []
347
+
348
+ // Calculate reset time and period from usage dates
349
+ const endDate = usage.endDate
350
+ const startDate = usage.startDate
351
+ const resetsAt = typeof endDate === "number" ? ctx.util.toIso(endDate) : null
352
+ const periodDurationMs = (typeof endDate === "number" && typeof startDate === "number")
353
+ ? (endDate - startDate)
354
+ : null
355
+
356
+ // Standard tokens (primary line)
357
+ const standard = usage.standard
358
+ if (standard && typeof standard.totalAllowance === "number") {
359
+ const used = standard.orgTotalTokensUsed || 0
360
+ const limit = standard.totalAllowance
361
+ lines.push(ctx.line.progress({
362
+ label: "Standard",
363
+ used: used,
364
+ limit: limit,
365
+ format: { kind: "count", suffix: "tokens" },
366
+ resetsAt: resetsAt,
367
+ periodDurationMs: periodDurationMs,
368
+ }))
369
+ }
370
+
371
+ // Premium tokens (detail line, only if plan includes premium)
372
+ const premium = usage.premium
373
+ if (premium && typeof premium.totalAllowance === "number" && premium.totalAllowance > 0) {
374
+ const used = premium.orgTotalTokensUsed || 0
375
+ const limit = premium.totalAllowance
376
+ lines.push(ctx.line.progress({
377
+ label: "Premium",
378
+ used: used,
379
+ limit: limit,
380
+ format: { kind: "count", suffix: "tokens" },
381
+ resetsAt: resetsAt,
382
+ periodDurationMs: periodDurationMs,
383
+ }))
384
+ }
385
+
386
+ // Infer plan from allowance
387
+ let plan = null
388
+ if (standard && typeof standard.totalAllowance === "number") {
389
+ const allowance = standard.totalAllowance
390
+ if (allowance >= 200000000) {
391
+ plan = "Max"
392
+ } else if (allowance >= 20000000) {
393
+ plan = "Pro"
394
+ } else if (allowance > 0) {
395
+ plan = "Basic"
396
+ }
397
+ }
398
+
399
+ if (lines.length === 0) {
400
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
401
+ }
402
+
403
+ return { plan: plan, lines: lines }
404
+ }
405
+
406
+ globalThis.__openusage_plugin = { id: "factory", probe }
407
+ })()
@@ -0,0 +1,19 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "factory",
4
+ "name": "Factory",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#020202",
9
+ "cli": {
10
+ "category": "cli",
11
+ "binaryName": "droid",
12
+ "installCmd": null,
13
+ "loginCmd": "droid auth login"
14
+ },
15
+ "lines": [
16
+ { "type": "progress", "label": "Standard", "scope": "overview", "primaryOrder": 1 },
17
+ { "type": "progress", "label": "Premium", "scope": "detail" }
18
+ ]
19
+ }