@goodandready/dsh-subscriptions 0.5.3 → 0.5.5

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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Session Cache & Token Analyzer.
3
+ * Parses Harness turn usage and reports cache efficiency.
4
+ */
5
+
6
+ export function analyzeSessionEvents(events = []) {
7
+ let promptTokens = 0
8
+ let cachedTokens = 0
9
+ let completionTokens = 0
10
+ let totalCalls = 0
11
+ const callRecords = []
12
+
13
+ let prevPrompt = 0
14
+ for (const ev of events) {
15
+ if (!ev) continue
16
+ const usage = ev.usage || (ev.type === 'usage' ? ev.usage : null) || (ev.data && ev.data.usage)
17
+ if (!usage) continue
18
+
19
+ const p = Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0)
20
+ const c = Number(usage.prompt_tokens_details?.cached_tokens || usage.cache_read_input_tokens || usage.cached_tokens || usage.cachedTokens || 0)
21
+ const comp = Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0)
22
+
23
+ if (p <= 0 && comp <= 0) continue
24
+
25
+ totalCalls++
26
+ promptTokens += p
27
+ cachedTokens += c
28
+ completionTokens += comp
29
+
30
+ let classification = 'cold_start'
31
+ if (totalCalls > 1) {
32
+ if (c > 0 && p > 0 && (c / p) >= 0.5) {
33
+ classification = 'cache_hit'
34
+ } else if (p > prevPrompt) {
35
+ classification = 'delta'
36
+ } else {
37
+ classification = 'affinity_miss'
38
+ }
39
+ }
40
+ prevPrompt = p
41
+
42
+ callRecords.push({
43
+ call: totalCalls,
44
+ prompt: p,
45
+ cached: c,
46
+ completion: comp,
47
+ classification,
48
+ hitRate: p > 0 ? Math.round((c / p) * 100) : 0,
49
+ })
50
+ }
51
+
52
+ const weightedCacheHitPercent = promptTokens > 0
53
+ ? Math.round((cachedTokens / promptTokens) * 1000) / 10
54
+ : 0
55
+
56
+ return {
57
+ totalCalls,
58
+ promptTokens,
59
+ cachedTokens,
60
+ completionTokens,
61
+ totalTokens: promptTokens + completionTokens,
62
+ weightedCacheHitPercent,
63
+ savedTokens: cachedTokens,
64
+ calls: callRecords,
65
+ }
66
+ }
@@ -97,6 +97,17 @@ export async function discoverLocalCliSessions() {
97
97
  }
98
98
  }
99
99
 
100
+
101
+ // 6. Cursor Token (env or CLI)
102
+ if (process.env.CURSOR_ACCESS_TOKEN) {
103
+ detected.cursor = {
104
+ provider: 'cursor',
105
+ path: 'CURSOR_ACCESS_TOKEN env',
106
+ email: 'Cursor IDE User',
107
+ hasRefreshToken: false,
108
+ }
109
+ }
110
+
100
111
  return detected
101
112
  }
102
113
 
@@ -159,5 +170,16 @@ export async function loadLocalCliBlob(provider) {
159
170
  }
160
171
  }
161
172
 
173
+
174
+ if (provider === 'cursor') {
175
+ const token = process.env.CURSOR_ACCESS_TOKEN || (raw && (raw.accessToken || raw.access_token)) || ''
176
+ return {
177
+ accessToken: token,
178
+ refreshToken: '',
179
+ expiresAt: Date.now() + 30 * 86400 * 1000,
180
+ email: 'Cursor User',
181
+ }
182
+ }
183
+
162
184
  throw new Error(`unsupported local CLI import for provider ${provider}`)
163
185
  }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { analyzeSessionEvents } from './analyze-session.js'
1
2
  import { discoverLocalCliSessions, loadLocalCliBlob } from './import-auth.js'
2
3
  import { readFileSync } from 'node:fs'
3
4
  import z from '@deepseek-ai/schemastery'
@@ -1170,6 +1171,26 @@ export function apply(ctx, config) {
1170
1171
  },
1171
1172
  }), 'dsh-subscriptions: /import-local')
1172
1173
 
1174
+ ctx.effect(() => ctx.webServer.register({
1175
+ kind: 'exact',
1176
+ path: '/dsh-subscriptions/analyze-session',
1177
+ handler: async (req, res) => {
1178
+ if (req.method !== 'POST') {
1179
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
1180
+ return
1181
+ }
1182
+ try {
1183
+ const body = await readBody(req).catch(() => ({}))
1184
+ const events = Array.isArray(body && body.events) ? body.events : []
1185
+ const analysis = analyzeSessionEvents(events)
1186
+ writeJson(res, 200, { ok: true, analysis })
1187
+ } catch (e) {
1188
+ writeJson(res, 500, { ok: false, error: { message: String(e && e.message || e) } })
1189
+ }
1190
+ },
1191
+ }), 'dsh-subscriptions: /analyze-session')
1192
+
1193
+
1173
1194
 
1174
1195
  // HTTP-прокси к API провайдера через subscriptions.request.
1175
1196
  // Same-origin only, allowlist путей, ротация и квота как у моделей.
package/lib/refs.js CHANGED
@@ -6,6 +6,7 @@ export const BUILTIN_PROVIDERS = Object.freeze([
6
6
  'antigravity',
7
7
  'kimi',
8
8
  'glm',
9
+ 'cursor',
9
10
  ])
10
11
 
11
12
  const dynamicIds = new Set()
@@ -43,6 +44,7 @@ const DISPLAY = {
43
44
  antigravity: 'Antigravity',
44
45
  kimi: 'Moonshot Kimi',
45
46
  glm: 'Zhipu GLM',
47
+ cursor: 'Cursor',
46
48
  }
47
49
 
48
50
  export function displayName(provider) {
@@ -0,0 +1,133 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm'
2
+ import { openaiMessages, openaiTools } from '../messages.js'
3
+ import { openaiChatStream, readJson, httpError } from '../wire.js'
4
+ import { asUsageSnapshot } from '../usage.js'
5
+
6
+ export const id = 'cursor'
7
+
8
+ export const CURSOR_API_BASE = 'https://api2.cursor.sh'
9
+ export const CURSOR_USAGE_URL = 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage'
10
+ export const CURSOR_AGENT_URL = 'https://agentn.us.api5.cursor.sh/agent.v1.AgentService/Run'
11
+ export const CURSOR_CLIENT_VERSION = 'cli-2026.05.01-eea359f'
12
+
13
+ export const CURSOR_MODELS = [
14
+ {
15
+ id: 'composer-2',
16
+ name: 'Composer 2',
17
+ contextWindow: 200000,
18
+ maxTokens: 64000,
19
+ inputModalities: ['text', 'image'],
20
+ reasoning: { efforts: [{ id: 'low', name: 'Low' }, { id: 'medium', name: 'Medium' }, { id: 'high', name: 'High' }, { id: 'max', name: 'Max' }] },
21
+ },
22
+ {
23
+ id: 'composer-1.5',
24
+ name: 'Composer 1.5',
25
+ contextWindow: 200000,
26
+ maxTokens: 64000,
27
+ inputModalities: ['text', 'image'],
28
+ },
29
+ {
30
+ id: 'claude-sonnet-5',
31
+ name: 'Claude Sonnet 5 (Cursor)',
32
+ contextWindow: 200000,
33
+ maxTokens: 64000,
34
+ inputModalities: ['text', 'image'],
35
+ },
36
+ {
37
+ id: 'gpt-5.5',
38
+ name: 'GPT-5.5 (Cursor)',
39
+ contextWindow: 200000,
40
+ maxTokens: 128000,
41
+ inputModalities: ['text'],
42
+ },
43
+ {
44
+ id: 'grok-4.5',
45
+ name: 'Grok 4.5 (Cursor)',
46
+ contextWindow: 200000,
47
+ maxTokens: 64000,
48
+ inputModalities: ['text'],
49
+ },
50
+ ]
51
+
52
+ export function providerInfo() {
53
+ return { id, name: 'Cursor' }
54
+ }
55
+
56
+ export function defaults() {
57
+ return {
58
+ apiBase: CURSOR_API_BASE,
59
+ models: CURSOR_MODELS.map((m) => m.id),
60
+ }
61
+ }
62
+
63
+ export function authorizeUrl() {
64
+ return 'https://cursor.com/loginDeepControl'
65
+ }
66
+
67
+ export async function listModels() {
68
+ return CURSOR_MODELS
69
+ }
70
+
71
+ export async function usage(blob, config, fetchImpl) {
72
+ try {
73
+ const impl = fetchImpl || fetch
74
+ const token = blob && (blob.accessToken || blob.access_token)
75
+ if (!token) return null
76
+ const res = await impl(CURSOR_USAGE_URL, {
77
+ method: 'POST',
78
+ headers: {
79
+ Authorization: `Bearer ${token}`,
80
+ 'x-cursor-client-version': CURSOR_CLIENT_VERSION,
81
+ 'User-Agent': CURSOR_CLIENT_VERSION,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: '{}',
85
+ })
86
+ if (!res.ok) return null
87
+ const json = await readJson(res)
88
+ // Cursor returns { numRequests, maxRequestUsage, plan }
89
+ if (json && json.numRequests != null && json.maxRequestUsage != null && json.maxRequestUsage > 0) {
90
+ const pct = Math.round((json.numRequests / json.maxRequestUsage) * 100)
91
+ const snap = asUsageSnapshot(pct)
92
+ if (snap) snap.plan = json.plan || 'Pro'
93
+ return snap
94
+ }
95
+ return null
96
+ } catch {
97
+ return null
98
+ }
99
+ }
100
+
101
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
102
+ const impl = fetchImpl || fetch
103
+ const token = blob && (blob.accessToken || blob.access_token)
104
+ if (!token) throw new LlmError('Cursor not authenticated', 'AUTH')
105
+
106
+ const body = {
107
+ model: options.model || 'composer-2',
108
+ messages: openaiMessages(options),
109
+ stream: true,
110
+ ...(options.maxTokens != null ? { max_tokens: options.maxTokens } : {}),
111
+ ...(options.temperature != null ? { temperature: options.temperature } : {}),
112
+ }
113
+
114
+ const tools = openaiTools(options)
115
+ if (tools && tools.length) body.tools = tools
116
+
117
+ const url = (config && config.agentUrl) || CURSOR_AGENT_URL
118
+ const res = await impl(url, {
119
+ method: 'POST',
120
+ headers: {
121
+ ...headers,
122
+ Authorization: `Bearer ${token}`,
123
+ 'x-cursor-client-version': CURSOR_CLIENT_VERSION,
124
+ 'User-Agent': CURSOR_CLIENT_VERSION,
125
+ 'Content-Type': 'application/json',
126
+ },
127
+ body: JSON.stringify(body),
128
+ signal,
129
+ })
130
+
131
+ if (!res.ok) throw httpError(res.status, await res.text())
132
+ yield* openaiChatStream(res.body)
133
+ }
@@ -1,3 +1,4 @@
1
+ import * as cursor from './cursor.js'
1
2
  import * as codex from './codex.js'
2
3
  import * as claude from './claude.js'
3
4
  import * as grok from './grok.js'
@@ -6,7 +7,7 @@ import * as kimi from './kimi.js'
6
7
  import * as glm from './glm.js'
7
8
  import { createVendorFromProfile } from '../vendor-factory.js'
8
9
 
9
- const builtins = { codex, claude, grok, antigravity, kimi, glm }
10
+ const builtins = { codex, claude, grok, antigravity, kimi, glm, cursor }
10
11
 
11
12
  const customVendors = new Map()
12
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",