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,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>
|