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,218 @@
1
+ (function () {
2
+ var LS_SERVICE = "exa.language_server_pb.LanguageServerService"
3
+
4
+ // Windsurf variants — tried in order (Windsurf first, then Windsurf Next).
5
+ // Markers use --ide_name exact matching in the Rust discover code.
6
+ var VARIANTS = [
7
+ {
8
+ marker: "windsurf",
9
+ ideName: "windsurf",
10
+ stateDb: "~/Library/Application Support/Windsurf/User/globalStorage/state.vscdb",
11
+ },
12
+ {
13
+ marker: "windsurf-next",
14
+ ideName: "windsurf-next",
15
+ stateDb: "~/Library/Application Support/Windsurf - Next/User/globalStorage/state.vscdb",
16
+ },
17
+ ]
18
+
19
+ // --- LS discovery ---
20
+
21
+ function discoverLs(ctx, variant) {
22
+ return ctx.host.ls.discover({
23
+ processName: "language_server_macos",
24
+ markers: [variant.marker],
25
+ csrfFlag: "--csrf_token",
26
+ portFlag: "--extension_server_port",
27
+ extraFlags: ["--windsurf_version"],
28
+ })
29
+ }
30
+
31
+ function loadApiKey(ctx, variant) {
32
+ try {
33
+ var rows = ctx.host.sqlite.query(
34
+ variant.stateDb,
35
+ "SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus' LIMIT 1"
36
+ )
37
+ var parsed = ctx.util.tryParseJson(rows)
38
+ if (!parsed || !parsed.length || !parsed[0].value) return null
39
+ var auth = ctx.util.tryParseJson(parsed[0].value)
40
+ if (!auth || !auth.apiKey) return null
41
+ return auth.apiKey
42
+ } catch (e) {
43
+ ctx.host.log.warn("failed to read API key from " + variant.marker + ": " + String(e))
44
+ return null
45
+ }
46
+ }
47
+
48
+ function probePort(ctx, scheme, port, csrf, ideName) {
49
+ ctx.host.http.request({
50
+ method: "POST",
51
+ url: scheme + "://127.0.0.1:" + port + "/" + LS_SERVICE + "/GetUnleashData",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Connect-Protocol-Version": "1",
55
+ "x-codeium-csrf-token": csrf,
56
+ },
57
+ bodyText: JSON.stringify({
58
+ context: {
59
+ properties: {
60
+ devMode: "false",
61
+ extensionVersion: "unknown",
62
+ ide: ideName,
63
+ ideVersion: "unknown",
64
+ os: "macos",
65
+ },
66
+ },
67
+ }),
68
+ timeoutMs: 5000,
69
+ dangerouslyIgnoreTls: scheme === "https",
70
+ })
71
+ return true
72
+ }
73
+
74
+ function findWorkingPort(ctx, discovery, ideName) {
75
+ var ports = discovery.ports || []
76
+ for (var i = 0; i < ports.length; i++) {
77
+ var port = ports[i]
78
+ try { if (probePort(ctx, "https", port, discovery.csrf, ideName)) return { port: port, scheme: "https" } } catch (e) { /* ignore */ }
79
+ try { if (probePort(ctx, "http", port, discovery.csrf, ideName)) return { port: port, scheme: "http" } } catch (e) { /* ignore */ }
80
+ ctx.host.log.info("port " + port + " probe failed on both schemes")
81
+ }
82
+ if (discovery.extensionPort) return { port: discovery.extensionPort, scheme: "http" }
83
+ return null
84
+ }
85
+
86
+ function callLs(ctx, port, scheme, csrf, method, body) {
87
+ var resp = ctx.host.http.request({
88
+ method: "POST",
89
+ url: scheme + "://127.0.0.1:" + port + "/" + LS_SERVICE + "/" + method,
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ "Connect-Protocol-Version": "1",
93
+ "x-codeium-csrf-token": csrf,
94
+ },
95
+ bodyText: JSON.stringify(body || {}),
96
+ timeoutMs: 10000,
97
+ dangerouslyIgnoreTls: scheme === "https",
98
+ })
99
+ if (resp.status < 200 || resp.status >= 300) {
100
+ ctx.host.log.warn("callLs " + method + " returned " + resp.status)
101
+ return null
102
+ }
103
+ return ctx.util.tryParseJson(resp.bodyText)
104
+ }
105
+
106
+ // --- Credit line builder ---
107
+
108
+ function creditLine(ctx, label, used, total, resetsAt, periodMs) {
109
+ if (typeof total !== "number" || total <= 0) return null
110
+ if (typeof used !== "number") used = 0
111
+ if (used < 0) used = 0
112
+ var line = {
113
+ label: label,
114
+ used: used,
115
+ limit: total,
116
+ format: { kind: "count", suffix: "credits" },
117
+ }
118
+ if (resetsAt) line.resetsAt = resetsAt
119
+ if (periodMs) line.periodDurationMs = periodMs
120
+ return ctx.line.progress(line)
121
+ }
122
+
123
+ // --- LS probe for a specific variant ---
124
+
125
+ function probeVariant(ctx, variant) {
126
+ var discovery = discoverLs(ctx, variant)
127
+ if (!discovery) return null
128
+
129
+ var found = findWorkingPort(ctx, discovery, variant.ideName)
130
+ if (!found) return null
131
+
132
+ var apiKey = loadApiKey(ctx, variant)
133
+ if (!apiKey) {
134
+ ctx.host.log.warn("no API key found in SQLite for " + variant.marker)
135
+ return null
136
+ }
137
+
138
+ var version = (discovery.extra && discovery.extra.windsurf_version) || "unknown"
139
+
140
+ var metadata = {
141
+ apiKey: apiKey,
142
+ ideName: variant.ideName,
143
+ ideVersion: version,
144
+ extensionName: variant.ideName,
145
+ extensionVersion: version,
146
+ locale: "en",
147
+ }
148
+
149
+ var data = null
150
+ try {
151
+ data = callLs(ctx, found.port, found.scheme, discovery.csrf, "GetUserStatus", { metadata: metadata })
152
+ } catch (e) {
153
+ ctx.host.log.warn("GetUserStatus threw for " + variant.marker + ": " + String(e))
154
+ }
155
+
156
+ if (!data || !data.userStatus) return null
157
+
158
+ var us = data.userStatus
159
+ var ps = us.planStatus || {}
160
+ var pi = ps.planInfo || {}
161
+
162
+ var plan = pi.planName || null
163
+
164
+ // Billing cycle for pacing
165
+ var planEnd = ps.planEnd || null
166
+ var planStart = ps.planStart || null
167
+ var periodMs = null
168
+ if (planStart && planEnd) {
169
+ var startMs = Date.parse(planStart)
170
+ var endMs = Date.parse(planEnd)
171
+ if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs) {
172
+ periodMs = endMs - startMs
173
+ }
174
+ }
175
+
176
+ var lines = []
177
+
178
+ // API returns credits in hundredths (like cents) — divide by 100 for display
179
+ // Windsurf UI: "0/500 prompt credits" = API availablePromptCredits: 50000
180
+
181
+ // Prompt credits
182
+ var promptTotal = ps.availablePromptCredits
183
+ var promptUsed = ps.usedPromptCredits || 0
184
+ if (typeof promptTotal === "number" && promptTotal > 0) {
185
+ var pl = creditLine(ctx, "Prompt credits", promptUsed / 100, promptTotal / 100, planEnd, periodMs)
186
+ if (pl) lines.push(pl)
187
+ }
188
+
189
+ // Flex credits
190
+ var flexTotal = ps.availableFlexCredits
191
+ var flexUsed = ps.usedFlexCredits || 0
192
+ if (typeof flexTotal === "number" && flexTotal > 0) {
193
+ var xl = creditLine(ctx, "Flex credits", flexUsed / 100, flexTotal / 100, null, null)
194
+ if (xl) lines.push(xl)
195
+ }
196
+
197
+ if (lines.length === 0) {
198
+ // All credits unlimited (negative available) — still return plan, show badge
199
+ lines.push(ctx.line.badge({ label: "Credits", text: "Unlimited" }))
200
+ }
201
+
202
+ return { plan: plan, lines: lines }
203
+ }
204
+
205
+ // --- Probe ---
206
+
207
+ function probe(ctx) {
208
+ // Try each variant in order: Windsurf → Windsurf Next
209
+ for (var i = 0; i < VARIANTS.length; i++) {
210
+ var result = probeVariant(ctx, VARIANTS[i])
211
+ if (result) return result
212
+ }
213
+
214
+ throw "Start Windsurf and try again."
215
+ }
216
+
217
+ globalThis.__openusage_plugin = { id: "windsurf", probe: probe }
218
+ })()
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "windsurf",
4
+ "name": "Windsurf",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#111111",
9
+ "cli": {
10
+ "category": "ide"
11
+ },
12
+ "lines": [
13
+ { "type": "progress", "label": "Prompt credits", "scope": "overview", "primaryOrder": 1 },
14
+ { "type": "progress", "label": "Flex credits", "scope": "overview" }
15
+ ]
16
+ }
@@ -0,0 +1,455 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { makeCtx } from "../test-helpers.js"
3
+
4
+ const loadPlugin = async () => {
5
+ await import("./plugin.js")
6
+ return globalThis.__openusage_plugin
7
+ }
8
+
9
+ // --- Fixtures ---
10
+
11
+ function makeDiscovery(overrides) {
12
+ return Object.assign(
13
+ { pid: 12345, csrf: "test-csrf", ports: [42001], extensionPort: null },
14
+ overrides
15
+ )
16
+ }
17
+
18
+ function makeAuthStatus(apiKey) {
19
+ return JSON.stringify([{ value: JSON.stringify({ apiKey: apiKey || "sk-ws-01-test" }) }])
20
+ }
21
+
22
+ function makeLsResponse(overrides) {
23
+ var base = {
24
+ userStatus: {
25
+ planStatus: {
26
+ planInfo: { planName: "Teams" },
27
+ planStart: "2026-01-18T09:07:17Z",
28
+ planEnd: "2026-02-18T09:07:17Z",
29
+ availablePromptCredits: 50000,
30
+ usedPromptCredits: 4700,
31
+ availableFlowCredits: 120000,
32
+ usedFlowCredits: 0,
33
+ availableFlexCredits: 2675000,
34
+ usedFlexCredits: 175550,
35
+ },
36
+ },
37
+ }
38
+ if (overrides) {
39
+ Object.assign(base.userStatus.planStatus, overrides)
40
+ }
41
+ return base
42
+ }
43
+
44
+ function setupLsMock(ctx, discovery, apiKey, responseBody, opts) {
45
+ var stateDb = (opts && opts.stateDb) || "Windsurf"
46
+ ctx.host.ls.discover.mockImplementation((discoverOpts) => {
47
+ // Match the right variant by marker
48
+ var marker = discoverOpts.markers[0]
49
+ if (marker === "windsurf" && stateDb === "Windsurf") return discovery
50
+ if (marker === "windsurf-next" && stateDb === "Windsurf - Next") return discovery
51
+ return null
52
+ })
53
+ ctx.host.sqlite.query.mockImplementation((db, sql) => {
54
+ if (String(sql).includes("windsurfAuthStatus") && String(db).includes(stateDb)) {
55
+ return makeAuthStatus(apiKey)
56
+ }
57
+ return "[]"
58
+ })
59
+ ctx.host.http.request.mockImplementation((reqOpts) => {
60
+ if (String(reqOpts.url).includes("GetUnleashData")) {
61
+ return { status: 200, bodyText: "{}" }
62
+ }
63
+ return { status: 200, bodyText: JSON.stringify(responseBody) }
64
+ })
65
+ }
66
+
67
+ // --- Tests ---
68
+
69
+ describe("windsurf plugin", () => {
70
+ beforeEach(() => {
71
+ delete globalThis.__openusage_plugin
72
+ vi.resetModules()
73
+ })
74
+
75
+ it("throws when LS not found and no cache", async () => {
76
+ const ctx = makeCtx()
77
+ ctx.host.ls.discover.mockReturnValue(null)
78
+ ctx.host.sqlite.query.mockReturnValue("[]")
79
+ const plugin = await loadPlugin()
80
+ expect(() => plugin.probe(ctx)).toThrow("Start Windsurf and try again.")
81
+ })
82
+
83
+ it("returns credits from LS with billing pacing", async () => {
84
+ const ctx = makeCtx()
85
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-test", makeLsResponse())
86
+
87
+ const plugin = await loadPlugin()
88
+ const result = plugin.probe(ctx)
89
+
90
+ expect(result.plan).toBe("Teams")
91
+
92
+ // Values divided by 100 for display (API stores in hundredths)
93
+ const prompt = result.lines.find((l) => l.label === "Prompt credits")
94
+ expect(prompt).toBeTruthy()
95
+ expect(prompt.used).toBe(47) // 4700 / 100
96
+ expect(prompt.limit).toBe(500) // 50000 / 100
97
+ expect(prompt.resetsAt).toBe("2026-02-18T09:07:17Z")
98
+ expect(prompt.periodDurationMs).toBeGreaterThan(0)
99
+
100
+ expect(result.lines.find((l) => l.label === "Flow credits")).toBeFalsy()
101
+
102
+ const flex = result.lines.find((l) => l.label === "Flex credits")
103
+ expect(flex).toBeTruthy()
104
+ expect(flex.used).toBe(1755.5) // 175550 / 100
105
+ expect(flex.limit).toBe(26750) // 2675000 / 100
106
+ expect(flex.resetsAt).toBeUndefined()
107
+ expect(flex.periodDurationMs).toBeUndefined()
108
+ })
109
+
110
+ it("flex credits have no billing cycle (non-renewing)", async () => {
111
+ const ctx = makeCtx()
112
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-test", makeLsResponse())
113
+
114
+ const plugin = await loadPlugin()
115
+ const result = plugin.probe(ctx)
116
+
117
+ const prompt = result.lines.find((l) => l.label === "Prompt credits")
118
+ expect(prompt.resetsAt).toBe("2026-02-18T09:07:17Z")
119
+ expect(prompt.periodDurationMs).toBeGreaterThan(0)
120
+
121
+ const flex = result.lines.find((l) => l.label === "Flex credits")
122
+ expect(flex.resetsAt).toBeUndefined()
123
+ expect(flex.periodDurationMs).toBeUndefined()
124
+ })
125
+
126
+ it("skips credit lines with negative available (unlimited)", async () => {
127
+ const ctx = makeCtx()
128
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-test", makeLsResponse({
129
+ availablePromptCredits: -1,
130
+ availableFlexCredits: 100000,
131
+ usedFlexCredits: 500,
132
+ }))
133
+
134
+ const plugin = await loadPlugin()
135
+ const result = plugin.probe(ctx)
136
+
137
+ expect(result.lines.find((l) => l.label === "Prompt credits")).toBeFalsy()
138
+ expect(result.lines.find((l) => l.label === "Flex credits")).toBeTruthy()
139
+ })
140
+
141
+ it("sends apiKey in metadata", async () => {
142
+ const ctx = makeCtx()
143
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-mykey", makeLsResponse())
144
+
145
+ let sentBody = null
146
+ ctx.host.http.request.mockImplementation((reqOpts) => {
147
+ if (String(reqOpts.url).includes("GetUserStatus")) {
148
+ sentBody = reqOpts.bodyText
149
+ }
150
+ if (String(reqOpts.url).includes("GetUnleashData")) {
151
+ return { status: 200, bodyText: "{}" }
152
+ }
153
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
154
+ })
155
+
156
+ const plugin = await loadPlugin()
157
+ plugin.probe(ctx)
158
+
159
+ expect(sentBody).toBeTruthy()
160
+ const parsed = JSON.parse(sentBody)
161
+ expect(parsed.metadata.apiKey).toBe("sk-ws-01-mykey")
162
+ expect(parsed.metadata.ideName).toBe("windsurf")
163
+ })
164
+
165
+ it("returns null from LS when no API key", async () => {
166
+ const ctx = makeCtx()
167
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery())
168
+ ctx.host.sqlite.query.mockReturnValue("[]")
169
+ ctx.host.http.request.mockImplementation((reqOpts) => {
170
+ if (String(reqOpts.url).includes("GetUnleashData")) {
171
+ return { status: 200, bodyText: "{}" }
172
+ }
173
+ return { status: 200, bodyText: "{}" }
174
+ })
175
+
176
+ const plugin = await loadPlugin()
177
+ // No API key → LS probe returns null → falls back to cache → no cache → throws
178
+ expect(() => plugin.probe(ctx)).toThrow("Start Windsurf and try again.")
179
+ })
180
+
181
+ it("calculates billing period duration from planStart/planEnd", async () => {
182
+ const ctx = makeCtx()
183
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-test", makeLsResponse())
184
+
185
+ const plugin = await loadPlugin()
186
+ const result = plugin.probe(ctx)
187
+
188
+ const prompt = result.lines.find((l) => l.label === "Prompt credits")
189
+ expect(prompt.used).toBe(47) // 4700 / 100
190
+ const expected = Date.parse("2026-02-18T09:07:17Z") - Date.parse("2026-01-18T09:07:17Z")
191
+ expect(prompt.periodDurationMs).toBe(expected)
192
+ })
193
+
194
+ // --- Windsurf Next tests ---
195
+
196
+ it("falls back to Windsurf Next when Windsurf LS not found", async () => {
197
+ const ctx = makeCtx()
198
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-next", makeLsResponse({
199
+ planInfo: { planName: "Pro" },
200
+ }), { stateDb: "Windsurf - Next" })
201
+
202
+ const plugin = await loadPlugin()
203
+ const result = plugin.probe(ctx)
204
+
205
+ expect(result.plan).toBe("Pro")
206
+ })
207
+
208
+ it("sends windsurf-next as ideName for Windsurf Next variant", async () => {
209
+ const ctx = makeCtx()
210
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-next", makeLsResponse(), { stateDb: "Windsurf - Next" })
211
+
212
+ let sentBody = null
213
+ const origHttp = ctx.host.http.request
214
+ ctx.host.http.request.mockImplementation((reqOpts) => {
215
+ if (String(reqOpts.url).includes("GetUserStatus")) {
216
+ sentBody = reqOpts.bodyText
217
+ }
218
+ if (String(reqOpts.url).includes("GetUnleashData")) {
219
+ return { status: 200, bodyText: "{}" }
220
+ }
221
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
222
+ })
223
+
224
+ const plugin = await loadPlugin()
225
+ plugin.probe(ctx)
226
+
227
+ expect(sentBody).toBeTruthy()
228
+ const parsed = JSON.parse(sentBody)
229
+ expect(parsed.metadata.ideName).toBe("windsurf-next")
230
+ expect(parsed.metadata.extensionName).toBe("windsurf-next")
231
+ })
232
+
233
+ it("reads API key from Windsurf Next SQLite path", async () => {
234
+ const ctx = makeCtx()
235
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-next", makeLsResponse(), { stateDb: "Windsurf - Next" })
236
+
237
+ let queriedDb = null
238
+ ctx.host.sqlite.query.mockImplementation((db, sql) => {
239
+ if (String(sql).includes("windsurfAuthStatus")) {
240
+ queriedDb = db
241
+ return makeAuthStatus("sk-ws-01-next")
242
+ }
243
+ return "[]"
244
+ })
245
+
246
+ const plugin = await loadPlugin()
247
+ plugin.probe(ctx)
248
+
249
+ expect(queriedDb).toContain("Windsurf - Next")
250
+ })
251
+
252
+ it("prefers Windsurf over Windsurf Next when both available", async () => {
253
+ const ctx = makeCtx()
254
+ // Both variants return valid discoveries
255
+ ctx.host.ls.discover.mockImplementation((discoverOpts) => {
256
+ return makeDiscovery()
257
+ })
258
+ ctx.host.sqlite.query.mockImplementation((db, sql) => {
259
+ if (String(sql).includes("windsurfAuthStatus")) {
260
+ return makeAuthStatus("sk-ws-01-both")
261
+ }
262
+ return "[]"
263
+ })
264
+ ctx.host.http.request.mockImplementation((reqOpts) => {
265
+ if (String(reqOpts.url).includes("GetUnleashData")) {
266
+ return { status: 200, bodyText: "{}" }
267
+ }
268
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
269
+ })
270
+
271
+ let sentBodies = []
272
+ ctx.host.http.request.mockImplementation((reqOpts) => {
273
+ if (String(reqOpts.url).includes("GetUserStatus")) {
274
+ sentBodies.push(reqOpts.bodyText)
275
+ }
276
+ if (String(reqOpts.url).includes("GetUnleashData")) {
277
+ return { status: 200, bodyText: "{}" }
278
+ }
279
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
280
+ })
281
+
282
+ const plugin = await loadPlugin()
283
+ const result = plugin.probe(ctx)
284
+
285
+ // Should use windsurf (first variant) and never try windsurf-next
286
+ expect(sentBodies.length).toBe(1)
287
+ const parsed = JSON.parse(sentBodies[0])
288
+ expect(parsed.metadata.ideName).toBe("windsurf")
289
+ })
290
+
291
+ it("falls back from https to http when probing LS port", async () => {
292
+ const ctx = makeCtx()
293
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-test", makeLsResponse())
294
+
295
+ ctx.host.http.request.mockImplementation((reqOpts) => {
296
+ const url = String(reqOpts.url)
297
+ if (url.includes("GetUnleashData")) {
298
+ if (url.startsWith("https://")) throw new Error("self-signed cert")
299
+ return { status: 200, bodyText: "{}" }
300
+ }
301
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
302
+ })
303
+
304
+ const plugin = await loadPlugin()
305
+ const result = plugin.probe(ctx)
306
+
307
+ expect(result.plan).toBe("Teams")
308
+ const unleashCalls = ctx.host.http.request.mock.calls
309
+ .map((call) => String(call[0]?.url))
310
+ .filter((url) => url.includes("GetUnleashData"))
311
+ expect(unleashCalls.some((url) => url.startsWith("http://"))).toBe(true)
312
+ })
313
+
314
+ it("uses extensionPort when direct probes fail for all discovered ports", async () => {
315
+ const ctx = makeCtx()
316
+ const discovery = makeDiscovery({ ports: [42001], extensionPort: 42002 })
317
+ setupLsMock(ctx, discovery, "sk-ws-01-test", makeLsResponse())
318
+
319
+ ctx.host.http.request.mockImplementation((reqOpts) => {
320
+ const url = String(reqOpts.url)
321
+ if (url.includes("GetUnleashData")) {
322
+ throw new Error("probe failed")
323
+ }
324
+ if (url.includes(":42002/") && url.includes("GetUserStatus")) {
325
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
326
+ }
327
+ return { status: 500, bodyText: "{}" }
328
+ })
329
+
330
+ const plugin = await loadPlugin()
331
+ const result = plugin.probe(ctx)
332
+ expect(result.plan).toBe("Teams")
333
+ })
334
+
335
+ it("returns unlimited badge when all credit buckets are non-positive", async () => {
336
+ const ctx = makeCtx()
337
+ setupLsMock(
338
+ ctx,
339
+ makeDiscovery(),
340
+ "sk-ws-01-test",
341
+ makeLsResponse({ availablePromptCredits: -1, availableFlexCredits: -1 })
342
+ )
343
+
344
+ const plugin = await loadPlugin()
345
+ const result = plugin.probe(ctx)
346
+
347
+ expect(result.lines).toEqual([{ type: "badge", label: "Credits", text: "Unlimited" }])
348
+ })
349
+
350
+ it("clamps negative used credits to zero", async () => {
351
+ const ctx = makeCtx()
352
+ setupLsMock(
353
+ ctx,
354
+ makeDiscovery(),
355
+ "sk-ws-01-test",
356
+ makeLsResponse({ availablePromptCredits: 50000, usedPromptCredits: -500, availableFlexCredits: -1 })
357
+ )
358
+
359
+ const plugin = await loadPlugin()
360
+ const result = plugin.probe(ctx)
361
+ const prompt = result.lines.find((l) => l.label === "Prompt credits")
362
+ expect(prompt.used).toBe(0)
363
+ })
364
+
365
+ it("omits billing period when plan dates are invalid", async () => {
366
+ const ctx = makeCtx()
367
+ setupLsMock(
368
+ ctx,
369
+ makeDiscovery(),
370
+ "sk-ws-01-test",
371
+ makeLsResponse({ planStart: "not-a-date", planEnd: "2026-02-18T09:07:17Z", availableFlexCredits: -1 })
372
+ )
373
+
374
+ const plugin = await loadPlugin()
375
+ const result = plugin.probe(ctx)
376
+ const prompt = result.lines.find((l) => l.label === "Prompt credits")
377
+ expect(prompt.periodDurationMs).toBeUndefined()
378
+ })
379
+
380
+ it("throws when GetUserStatus returns non-2xx", async () => {
381
+ const ctx = makeCtx()
382
+ setupLsMock(ctx, makeDiscovery(), "sk-ws-01-test", makeLsResponse())
383
+
384
+ ctx.host.http.request.mockImplementation((reqOpts) => {
385
+ if (String(reqOpts.url).includes("GetUnleashData")) {
386
+ return { status: 200, bodyText: "{}" }
387
+ }
388
+ return { status: 500, bodyText: "{}" }
389
+ })
390
+
391
+ const plugin = await loadPlugin()
392
+ expect(() => plugin.probe(ctx)).toThrow("Start Windsurf and try again.")
393
+ })
394
+
395
+ it("treats missing apiKey in SQLite auth payload as unavailable", async () => {
396
+ const ctx = makeCtx()
397
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery())
398
+ ctx.host.sqlite.query.mockReturnValue(JSON.stringify([{ value: JSON.stringify({}) }]))
399
+ ctx.host.http.request.mockImplementation((reqOpts) => {
400
+ if (String(reqOpts.url).includes("GetUnleashData")) {
401
+ return { status: 200, bodyText: "{}" }
402
+ }
403
+ return { status: 200, bodyText: "{}" }
404
+ })
405
+
406
+ const plugin = await loadPlugin()
407
+ expect(() => plugin.probe(ctx)).toThrow("Start Windsurf and try again.")
408
+ })
409
+
410
+ it("uses extensionPort when ports list is missing", async () => {
411
+ const ctx = makeCtx()
412
+ const discovery = makeDiscovery({ ports: undefined, extensionPort: 42002 })
413
+ setupLsMock(ctx, discovery, "sk-ws-01-test", makeLsResponse())
414
+ ctx.host.http.request.mockImplementation((reqOpts) => {
415
+ const url = String(reqOpts.url)
416
+ if (url.includes(":42002/") && url.includes("GetUserStatus")) {
417
+ return { status: 200, bodyText: JSON.stringify(makeLsResponse()) }
418
+ }
419
+ if (url.includes("GetUnleashData")) {
420
+ return { status: 500, bodyText: "{}" }
421
+ }
422
+ return { status: 500, bodyText: "{}" }
423
+ })
424
+
425
+ const plugin = await loadPlugin()
426
+ const result = plugin.probe(ctx)
427
+ expect(result.plan).toBe("Teams")
428
+ })
429
+
430
+ it("fails when all probes fail and extensionPort is absent", async () => {
431
+ const ctx = makeCtx()
432
+ const discovery = makeDiscovery({ ports: [42001], extensionPort: null })
433
+ setupLsMock(ctx, discovery, "sk-ws-01-test", makeLsResponse())
434
+ ctx.host.http.request.mockImplementation((reqOpts) => {
435
+ if (String(reqOpts.url).includes("GetUnleashData")) {
436
+ throw new Error("probe failed")
437
+ }
438
+ return { status: 500, bodyText: "{}" }
439
+ })
440
+
441
+ const plugin = await loadPlugin()
442
+ expect(() => plugin.probe(ctx)).toThrow("Start Windsurf and try again.")
443
+ })
444
+
445
+ it("handles sparse userStatus payload and returns unlimited badge", async () => {
446
+ const ctx = makeCtx()
447
+ setupLsMock(ctx, makeDiscovery({ extra: null }), "sk-ws-01-test", { userStatus: {} })
448
+
449
+ const plugin = await loadPlugin()
450
+ const result = plugin.probe(ctx)
451
+ expect(result.plan).toBeNull()
452
+ expect(result.lines).toEqual([{ type: "badge", label: "Credits", text: "Unlimited" }])
453
+ })
454
+
455
+ })
@@ -0,0 +1,5 @@
1
+ <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M51.57 23.67l-4.33 6.17c-0.67 0.97-1.80 1.57-3.00 1.57H20.57V23.63C20.53 23.67 51.57 23.67 51.57 23.67z" fill="currentColor"/>
3
+ <polygon points="81.00,23.67 43.80,76.37 19.00,76.37 56.20,23.67" fill="currentColor"/>
4
+ <path d="M48.43 76.37l4.37-6.20c0.67-0.97 1.80-1.57 3.00-1.57h23.63v7.77H48.43z" fill="currentColor"/>
5
+ </svg>