@gotcos/glasses-server 6.16.0 → 6.16.2

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,300 @@
1
+ import crypto from 'node:crypto'
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
3
+ import { dirname, resolve } from 'node:path'
4
+ import { CURSOR_ENGINE_SESSION_TTL_MS } from './cursor-engine-sessions.js'
5
+ import {
6
+ CURSOR_GROK_MODEL,
7
+ type CursorModelPreference,
8
+ } from '../../shared/model-preference.js'
9
+ import {
10
+ getCursorModelCatalogSnapshot,
11
+ resolveCursorModelOption,
12
+ } from './cursor-model-catalog.js'
13
+ import { getCodexExecutionCwd } from './codex-run-ledger.js'
14
+
15
+ const DEFAULT_MAX_RUNS = 100
16
+ const DEFAULT_TTL_MS = 7 * 24 * 60 * 60_000
17
+ const ERROR_PREVIEW_CHARS = 160
18
+ const RUNNING_STALE_MS = 30 * 60_000
19
+
20
+ function getProcessStartedAtMs(): number {
21
+ return Date.now() - Math.floor(process.uptime() * 1000)
22
+ }
23
+
24
+ export type CursorRunStatus =
25
+ | 'running'
26
+ | 'completed'
27
+ | 'failed'
28
+ | 'cancelled'
29
+ | 'client_disconnected'
30
+
31
+ export interface CursorRunRecord {
32
+ runId: string
33
+ turnId?: string
34
+ clientJobId?: string
35
+ cosSessionId: string
36
+ cursorChatId?: string
37
+ status: CursorRunStatus
38
+ createdAt: string
39
+ updatedAt: string
40
+ model: CursorModelPreference
41
+ cliModel: string
42
+ cwd: string
43
+ resumed?: boolean
44
+ expiresAt?: string
45
+ resumeCommand?: string
46
+ queryPreview?: string
47
+ outputPreview?: string
48
+ errorCode?: string
49
+ errorPreview?: string
50
+ durationMs?: number
51
+ exitCode?: number | null
52
+ messageEra?: string
53
+ globalMsgNum?: number
54
+ hasPersistedTerminalPatch?: boolean
55
+ }
56
+
57
+ interface CursorRunEvent {
58
+ runId: string
59
+ ts: string
60
+ patch: Partial<CursorRunRecord>
61
+ }
62
+
63
+ export interface CursorRunConfig {
64
+ cliModel: string
65
+ catalogSource: string
66
+ availableModels: Array<{ preference: CursorModelPreference; model: string; displayName: string }>
67
+ persistenceEnabled: boolean
68
+ cwd: string
69
+ engineSessionTtlMinutes: number
70
+ historyLimit: number
71
+ historyTtlDays: number
72
+ contentPreviewsEnabled: boolean
73
+ }
74
+
75
+ export function isCursorPersistenceEnabled(): boolean {
76
+ return process.env.COS_CURSOR_PERSIST_SESSIONS !== '0'
77
+ }
78
+
79
+ export function areCursorContentPreviewsEnabled(): boolean {
80
+ return process.env.COS_CURSOR_RUN_CONTENT_PREVIEWS === '1'
81
+ }
82
+
83
+ /** Same workspace resolver as Codex — Cursor --workspace uses this path. */
84
+ export function getCursorExecutionCwd(): string {
85
+ return getCodexExecutionCwd()
86
+ }
87
+
88
+ export function getCursorRunConfig(): CursorRunConfig {
89
+ const catalog = getCursorModelCatalogSnapshot()
90
+ const grok = resolveCursorModelOption(CURSOR_GROK_MODEL)
91
+ return {
92
+ cliModel: grok?.id || 'cursor-cli-default',
93
+ catalogSource: catalog.source,
94
+ availableModels: catalog.options.map(option => ({
95
+ preference: option.preference,
96
+ model: option.id || 'cursor-cli-default',
97
+ displayName: option.displayName,
98
+ })),
99
+ persistenceEnabled: isCursorPersistenceEnabled(),
100
+ cwd: getCursorExecutionCwd(),
101
+ engineSessionTtlMinutes: Math.round(CURSOR_ENGINE_SESSION_TTL_MS / 60_000),
102
+ historyLimit: getMaxRuns(),
103
+ historyTtlDays: Math.round(getTtlMs() / (24 * 60 * 60_000)),
104
+ contentPreviewsEnabled: areCursorContentPreviewsEnabled(),
105
+ }
106
+ }
107
+
108
+ export function getCursorLedgerPath(): string {
109
+ return resolve(process.env.COS_CURSOR_RUN_LEDGER_FILE || resolve(import.meta.dirname, '..', 'data', 'cursor-runs.jsonl'))
110
+ }
111
+
112
+ function getMaxRuns(): number {
113
+ const raw = Number(process.env.COS_CURSOR_RUN_LEDGER_MAX ?? DEFAULT_MAX_RUNS)
114
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_RUNS
115
+ }
116
+
117
+ function getTtlMs(): number {
118
+ const rawDays = Number(process.env.COS_CURSOR_RUN_LEDGER_TTL_DAYS ?? 7)
119
+ return Number.isFinite(rawDays) && rawDays > 0 ? rawDays * 24 * 60 * 60_000 : DEFAULT_TTL_MS
120
+ }
121
+
122
+ function appendEvent(event: CursorRunEvent): void {
123
+ try {
124
+ const path = getCursorLedgerPath()
125
+ mkdirSync(dirname(path), { recursive: true })
126
+ appendFileSync(path, JSON.stringify(event) + '\n')
127
+ } catch (err) {
128
+ console.warn('[cursor-run-ledger] write skipped:', err)
129
+ }
130
+ }
131
+
132
+ function readEvents(): CursorRunEvent[] {
133
+ const path = getCursorLedgerPath()
134
+ if (!existsSync(path)) return []
135
+ try {
136
+ const events: CursorRunEvent[] = []
137
+ for (const line of readFileSync(path, 'utf-8')
138
+ .split('\n')
139
+ .map(line => line.trim())
140
+ .filter(Boolean)) {
141
+ try {
142
+ const event = JSON.parse(line) as CursorRunEvent
143
+ if (typeof event.runId === 'string' && typeof event.ts === 'string' && typeof event.patch === 'object') {
144
+ events.push(event)
145
+ }
146
+ } catch {
147
+ // Skip torn/corrupt JSONL rows.
148
+ }
149
+ }
150
+ return events
151
+ } catch {
152
+ return []
153
+ }
154
+ }
155
+
156
+ function hydrateRuns(): CursorRunRecord[] {
157
+ const runs = new Map<string, CursorRunRecord>()
158
+ const order = new Map<string, number>()
159
+ let eventIndex = 0
160
+ for (const event of readEvents()) {
161
+ eventIndex += 1
162
+ const existing = runs.get(event.runId)
163
+ const next = { ...(existing ?? {}), ...event.patch, runId: event.runId } as CursorRunRecord
164
+ if (next.cursorChatId && !next.resumeCommand) {
165
+ next.resumeCommand = `agent --resume ${next.cursorChatId}`
166
+ }
167
+ runs.set(event.runId, next)
168
+ order.set(event.runId, eventIndex)
169
+ }
170
+
171
+ const cutoff = Date.now() - getTtlMs()
172
+ return Array.from(runs.values())
173
+ .filter(run => run.createdAt && Date.parse(run.updatedAt || run.createdAt) >= cutoff)
174
+ .map(run => {
175
+ const updatedMs = Date.parse(run.updatedAt || run.createdAt)
176
+ const predatesCurrentProcess = updatedMs < getProcessStartedAtMs() - 1000
177
+ if (run.status === 'running' && (predatesCurrentProcess || Date.now() - updatedMs > RUNNING_STALE_MS)) {
178
+ return {
179
+ ...run,
180
+ status: 'client_disconnected' as const,
181
+ errorCode: run.errorCode ?? 'cursor.interrupted',
182
+ }
183
+ }
184
+ return run
185
+ })
186
+ .sort((a, b) => {
187
+ const byCreated = Date.parse(b.createdAt) - Date.parse(a.createdAt)
188
+ if (byCreated !== 0) return byCreated
189
+ return (order.get(b.runId) ?? 0) - (order.get(a.runId) ?? 0)
190
+ })
191
+ .slice(0, getMaxRuns())
192
+ }
193
+
194
+ export function redactForCursorLedger(value: string, maxChars = ERROR_PREVIEW_CHARS): string {
195
+ return value
196
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[email]')
197
+ .replace(/\b(?:sk|sess|ghp|github_pat|glpat|keycurs)-[A-Za-z0-9_\-]{12,}\b/g, '[token]')
198
+ .replace(/\bBearer\s+[A-Za-z0-9._\-]{12,}\b/gi, 'Bearer [token]')
199
+ .replace(/[A-Za-z0-9+/=]{80,}/g, '[blob]')
200
+ .replace(/\s+/g, ' ')
201
+ .trim()
202
+ .slice(0, maxChars)
203
+ }
204
+
205
+ export function classifyCursorError(message: string): string {
206
+ const text = message.toLowerCase()
207
+ if (/command not found|enoent|not found|agent binary/.test(text)) return 'cursor.cli_unavailable'
208
+ if (/permission|denied|sandbox|read-only|operation not permitted/.test(text)) return 'cursor.permission_denied'
209
+ if (/auth|login|sign in|unauthorized|forbidden|token|(?:api|http|error)\s*(?:error)?\s*[:=-]?\s*(?:401|403)\b/.test(text)) {
210
+ return 'cursor.auth_error'
211
+ }
212
+ if (/timeout|timed out|wall clock|no output/.test(text)) return 'cursor.timeout'
213
+ if (/exit\s+\d+/.test(text)) return 'cursor.nonzero_exit'
214
+ return 'cursor.error'
215
+ }
216
+
217
+ export function startCursorRun(input: {
218
+ turnId?: string
219
+ clientJobId?: string
220
+ cosSessionId: string
221
+ model: CursorModelPreference
222
+ cwd: string
223
+ resumed?: boolean
224
+ cursorChatId?: string
225
+ expiresAt?: string
226
+ query: string
227
+ cliModel?: string
228
+ messageEra?: string
229
+ globalMsgNum?: number
230
+ }): CursorRunRecord {
231
+ const now = new Date().toISOString()
232
+ const run: CursorRunRecord = {
233
+ runId: `cursor-${crypto.randomUUID().slice(0, 8)}`,
234
+ turnId: input.turnId,
235
+ clientJobId: input.clientJobId,
236
+ cosSessionId: input.cosSessionId,
237
+ status: 'running',
238
+ createdAt: now,
239
+ updatedAt: now,
240
+ model: input.model,
241
+ cliModel: input.cliModel ?? (resolveCursorModelOption(input.model)?.id || 'cursor-cli-default'),
242
+ cwd: input.cwd,
243
+ resumed: input.resumed,
244
+ cursorChatId: input.cursorChatId,
245
+ expiresAt: input.expiresAt,
246
+ messageEra: input.messageEra,
247
+ globalMsgNum: input.globalMsgNum,
248
+ hasPersistedTerminalPatch: false,
249
+ }
250
+ if (areCursorContentPreviewsEnabled()) {
251
+ run.queryPreview = redactForCursorLedger(input.query)
252
+ }
253
+ appendEvent({ runId: run.runId, ts: now, patch: run })
254
+ return run
255
+ }
256
+
257
+ export function updateCursorRun(runId: string, patch: Partial<Omit<CursorRunRecord, 'runId' | 'createdAt'>>): CursorRunRecord | null {
258
+ const ts = new Date().toISOString()
259
+ const safePatch = { ...patch, updatedAt: ts }
260
+ if (safePatch.cursorChatId && !safePatch.resumeCommand) {
261
+ safePatch.resumeCommand = `agent --resume ${safePatch.cursorChatId}`
262
+ }
263
+ appendEvent({ runId, ts, patch: safePatch })
264
+ return getCursorRun(runId)
265
+ }
266
+
267
+ export function finishCursorRun(runId: string, input: {
268
+ status: Exclude<CursorRunStatus, 'running'>
269
+ startedAtMs: number
270
+ output?: string
271
+ error?: string
272
+ exitCode?: number | null
273
+ }): CursorRunRecord | null {
274
+ const patch: Partial<CursorRunRecord> = {
275
+ status: input.status,
276
+ hasPersistedTerminalPatch: true,
277
+ durationMs: Math.max(0, Date.now() - input.startedAtMs),
278
+ exitCode: input.exitCode,
279
+ }
280
+ if (input.output && areCursorContentPreviewsEnabled()) {
281
+ patch.outputPreview = redactForCursorLedger(input.output)
282
+ }
283
+ if (input.error) {
284
+ patch.errorCode = classifyCursorError(input.error)
285
+ if (areCursorContentPreviewsEnabled()) {
286
+ patch.errorPreview = redactForCursorLedger(input.error)
287
+ }
288
+ }
289
+ return updateCursorRun(runId, patch)
290
+ }
291
+
292
+ export function listCursorRuns(limit = 20, cosSessionId?: string): CursorRunRecord[] {
293
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(Math.floor(limit), getMaxRuns()) : 20
294
+ const runs = hydrateRuns()
295
+ return (cosSessionId ? runs.filter(run => run.cosSessionId === cosSessionId) : runs).slice(0, safeLimit)
296
+ }
297
+
298
+ export function getCursorRun(runId: string): CursorRunRecord | null {
299
+ return hydrateRuns().find(run => run.runId === runId) ?? null
300
+ }
@@ -343,7 +343,7 @@ function getVocabEchoMatcher(): RegExp {
343
343
  // Only UNAMBIGUOUS terms trigger an echo drop: multi-word phrases ("POS Nation",
344
344
  // "IT Retail", "Jeremy Sokolic") and brand-shaped single tokens with an internal
345
345
  // capital or digit ("POSNation", "CaratIQ", "Jewel360"). Plain single-word tokens
346
- // ("Austin", "Miles", "Ukaoma") are common words / ambiguous and are EXCLUDED
346
+ // Common words and ambiguous proper nouns are deliberately excluded
347
347
  // they carry too much false-drop risk for an always-on list rule.
348
348
  const terms = [...raw].filter(t => {
349
349
  if (t.length < 2) return false
@@ -1,5 +1,6 @@
1
1
  import { callClaudeStreaming, type CallOptions, type StreamCallbacks } from './claude-bridge.js'
2
2
  import { callCodexStreaming } from './codex-bridge.js'
3
+ import { callCursorStreaming } from './cursor-bridge.js'
3
4
  import {
4
5
  getOrCreateSession,
5
6
  getSessionModel,
@@ -7,7 +8,18 @@ import {
7
8
  type ModelPreference,
8
9
  type PromptReference,
9
10
  } from './conversation.js'
10
- import { DEFAULT_MODEL, isCodexModel, isClaudeModel, normalizeModelPreference } from '../../shared/model-preference.js'
11
+ import {
12
+ DEFAULT_MODEL,
13
+ isCodexModel,
14
+ isClaudeModel,
15
+ isCursorModel,
16
+ normalizeModelPreference,
17
+ } from '../../shared/model-preference.js'
18
+ import {
19
+ getCursorModelCatalog,
20
+ isCursorProviderReady,
21
+ resolveCursorModelOption,
22
+ } from './cursor-model-catalog.js'
11
23
  import type { ModelImageInput } from './model-image-input.js'
12
24
 
13
25
  // Bridges return as soon as their subprocess is spawned, while completion is
@@ -87,6 +99,19 @@ export async function callModelStreaming(
87
99
  }
88
100
 
89
101
  try {
102
+ // Cursor slots fail closed — never fall through to Claude/Codex.
103
+ if (isCursorModel(resolvedModel)) {
104
+ await getCursorModelCatalog()
105
+ const option = resolveCursorModelOption(resolvedModel)
106
+ if (!isCursorProviderReady() || !option?.id) {
107
+ const message = !option?.id
108
+ ? `cursor-bridge: Cursor model slot ${resolvedModel} is not resolved. Check agent models / login.`
109
+ : 'cursor-bridge: Cursor CLI unavailable. Install agent and run agent login.'
110
+ await lockedCallbacks.onError(message)
111
+ return sid
112
+ }
113
+ return await callCursorStreaming(query, sid, lockedCallbacks, resolvedModel, images, reference, globalMsgNum, options)
114
+ }
90
115
  if (isCodexModel(resolvedModel)) {
91
116
  return await callCodexStreaming(query, sid, lockedCallbacks, resolvedModel, images, reference, globalMsgNum, options)
92
117
  }
@@ -1,4 +1,4 @@
1
- export type CliProvider = 'claude' | 'codex'
1
+ export type CliProvider = 'claude' | 'codex' | 'cursor'
2
2
 
3
3
  const AUTH_CODE = /^(?:401|403|unauthori[sz]ed|forbidden|authentication_error|authorization_error|invalid_api_key|not_authenticated)$/i
4
4
  // Match whole, terminal-looking provider failures only. Natural assistant
@@ -292,6 +292,7 @@ export class QueryJobCoordinator {
292
292
  claudeRunId: snapshot.claudeRunId,
293
293
  codexRunId: snapshot.codexRunId,
294
294
  codexThreadId: snapshot.codexThreadId,
295
+ cursorRunId: snapshot.cursorRunId,
295
296
  })
296
297
  // Generation is already durable, but bridge post-processing missed
297
298
  // its deadline. Abort that tail after committing the answer so the
@@ -10,6 +10,7 @@ import { QueryJobCoordinator, type QueryJobRunner } from './query-job-coordinato
10
10
  import { QueryJobStore } from './query-job-store.js'
11
11
  import {
12
12
  isCodexModel,
13
+ isCursorModel,
13
14
  normalizeEffortPreference,
14
15
  normalizeModelPreference,
15
16
  type ModelPreference,
@@ -48,7 +49,8 @@ export async function preparePublicDurableQueryAdmission(raw: unknown): Promise<
48
49
  }
49
50
  }
50
51
 
51
- function providerFor(model: ModelPreference): 'claude' | 'codex' {
52
+ function providerFor(model: ModelPreference): 'claude' | 'codex' | 'cursor' {
53
+ if (isCursorModel(model)) return 'cursor'
52
54
  return isCodexModel(model) ? 'codex' : 'claude'
53
55
  }
54
56
 
@@ -122,6 +124,7 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
122
124
  claudeRunId: metadata?.claudeRunId,
123
125
  codexRunId: metadata?.codexRunId,
124
126
  codexThreadId: metadata?.codexThreadId,
127
+ cursorRunId: metadata?.cursorRunId,
125
128
  } as const
126
129
  await callbacks.onStart({ sessionId, ...linkage })
127
130
  emitDisplay({ type: 'start', data: {
@@ -142,7 +145,9 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
142
145
  ...(activeModel ? { resolvedModel: activeModel } : {}),
143
146
  ...(metadata.provider === 'claude'
144
147
  ? { claudeRunId: metadata.runId }
145
- : { codexRunId: metadata.runId }),
148
+ : metadata.provider === 'cursor'
149
+ ? { cursorRunId: metadata.runId }
150
+ : { codexRunId: metadata.runId }),
146
151
  }),
147
152
  onChunk: text => { callbacks.onChunk(text) },
148
153
  onToolStatus: toolName => {
@@ -175,6 +180,7 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
175
180
  claudeRunId: metadata?.claudeRunId,
176
181
  codexRunId: metadata?.codexRunId,
177
182
  codexThreadId: metadata?.codexThreadId,
183
+ cursorRunId: metadata?.cursorRunId,
178
184
  } as const
179
185
  // Publish compatibility completion only after the durable terminal is
180
186
  // fsynced. Display subscribers can disappear without owning this job.
@@ -361,6 +361,7 @@ export class QueryJobStore {
361
361
  claudeRunId: snapshot.claudeRunId,
362
362
  codexRunId: snapshot.codexRunId,
363
363
  codexThreadId: snapshot.codexThreadId,
364
+ cursorRunId: snapshot.cursorRunId,
364
365
  })
365
366
  } else {
366
367
  const result = await this.interrupt(snapshot.jobId, 'server_restarted')
@@ -548,9 +549,11 @@ export class QueryJobStore {
548
549
  }
549
550
 
550
551
  private applyLinkage(snapshot: QueryJobSnapshot, raw: Record<string, unknown>): void {
551
- const provider = raw.provider === 'claude' || raw.provider === 'codex' ? raw.provider : undefined
552
+ const provider = raw.provider === 'claude' || raw.provider === 'codex' || raw.provider === 'cursor'
553
+ ? raw.provider
554
+ : undefined
552
555
  if (provider) snapshot.provider = provider
553
- const fields = ['resolvedModel', 'cliSessionId', 'claudeRunId', 'codexRunId', 'codexThreadId'] as const
556
+ const fields = ['resolvedModel', 'cliSessionId', 'claudeRunId', 'codexRunId', 'codexThreadId', 'cursorRunId'] as const
554
557
  for (const field of fields) {
555
558
  const value = safeOptional(raw[field])
556
559
  if (value) snapshot[field] = value
@@ -795,6 +798,7 @@ export class QueryJobStore {
795
798
  claudeRunId: current.claudeRunId,
796
799
  codexRunId: current.codexRunId,
797
800
  codexThreadId: current.codexThreadId,
801
+ cursorRunId: current.cursorRunId,
798
802
  })
799
803
  }
800
804
  const normalized = normalizeQueryJobError(error)
@@ -831,6 +835,7 @@ export class QueryJobStore {
831
835
  claudeRunId: current.claudeRunId,
832
836
  codexRunId: current.codexRunId,
833
837
  codexThreadId: current.codexThreadId,
838
+ cursorRunId: current.cursorRunId,
834
839
  })
835
840
  }
836
841
  const error: QueryJobError = {
@@ -880,12 +885,15 @@ export class QueryJobStore {
880
885
 
881
886
  private safeLinkage(linkage: QueryJobProviderLinkage): QueryJobProviderLinkage {
882
887
  return {
883
- ...(linkage.provider === 'claude' || linkage.provider === 'codex' ? { provider: linkage.provider } : {}),
888
+ ...(linkage.provider === 'claude' || linkage.provider === 'codex' || linkage.provider === 'cursor'
889
+ ? { provider: linkage.provider }
890
+ : {}),
884
891
  ...(safeOptional(linkage.resolvedModel, 64) ? { resolvedModel: safeOptional(linkage.resolvedModel, 64) } : {}),
885
892
  ...(safeOptional(linkage.cliSessionId) ? { cliSessionId: safeOptional(linkage.cliSessionId) } : {}),
886
893
  ...(safeOptional(linkage.claudeRunId) ? { claudeRunId: safeOptional(linkage.claudeRunId) } : {}),
887
894
  ...(safeOptional(linkage.codexRunId) ? { codexRunId: safeOptional(linkage.codexRunId) } : {}),
888
895
  ...(safeOptional(linkage.codexThreadId) ? { codexThreadId: safeOptional(linkage.codexThreadId) } : {}),
896
+ ...(safeOptional(linkage.cursorRunId) ? { cursorRunId: safeOptional(linkage.cursorRunId) } : {}),
889
897
  }
890
898
  }
891
899
 
@@ -73,12 +73,13 @@ export interface QueryJobRequest {
73
73
  }
74
74
 
75
75
  export interface QueryJobProviderLinkage {
76
- provider?: 'claude' | 'codex'
76
+ provider?: 'claude' | 'codex' | 'cursor'
77
77
  resolvedModel?: string
78
78
  cliSessionId?: string
79
79
  claudeRunId?: string
80
80
  codexRunId?: string
81
81
  codexThreadId?: string
82
+ cursorRunId?: string
82
83
  }
83
84
 
84
85
  /** Path/id-free aggregate from output-image finalization. Values are bounded
@@ -78,7 +78,7 @@ export interface TrainingStatus {
78
78
 
79
79
  function getApiKey(): string {
80
80
  const key = loadCosEnvKey('FIREFLIES_API_KEY')
81
- if (!key) throw new Error('FIREFLIES_API_KEY not found — set it in MU-Chief-Staff/.env')
81
+ if (!key) throw new Error('FIREFLIES_API_KEY not found — set it in your COS environment')
82
82
  return key
83
83
  }
84
84
 
@@ -23,11 +23,16 @@ function estimateTokens(chars: number): number {
23
23
 
24
24
  export interface TokenAuditEntry {
25
25
  source: string // "g2-voice", "g2-prewarm", "g2-archive", "g2-query"
26
- model: string // "opus", "sonnet", "haiku"
26
+ model: string // "opus", "sonnet", "haiku", "cursor-composer", ...
27
27
  inputChars: number
28
28
  outputChars: number
29
29
  durationMs: number
30
30
  caller: string // "voice_query", "prewarm", "chat_summary", "day_summary"
31
+ turnId?: string
32
+ runId?: string
33
+ sessionId?: string
34
+ clientJobId?: string
35
+ usageKind?: 'provider_final' | 'estimated'
31
36
  }
32
37
 
33
38
  export function logTokenAudit(entry: TokenAuditEntry): void {
@@ -41,6 +46,11 @@ export function logTokenAudit(entry: TokenAuditEntry): void {
41
46
  est_output_tokens: estimateTokens(entry.outputChars),
42
47
  duration_ms: entry.durationMs,
43
48
  caller: entry.caller,
49
+ usage_kind: entry.usageKind ?? 'estimated',
50
+ ...(entry.turnId ? { turn_id: entry.turnId } : {}),
51
+ ...(entry.runId ? { run_id: entry.runId } : {}),
52
+ ...(entry.sessionId ? { session_id: entry.sessionId } : {}),
53
+ ...(entry.clientJobId ? { client_job_id: entry.clientJobId } : {}),
44
54
  }
45
55
  try {
46
56
  appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 })
Binary file
@@ -8,6 +8,10 @@ import {
8
8
  getCodexRunConfig,
9
9
  listCodexRuns,
10
10
  } from '../lib/codex-run-ledger.js'
11
+ import {
12
+ getCursorRunConfig,
13
+ listCursorRuns,
14
+ } from '../lib/cursor-run-ledger.js'
11
15
  import {
12
16
  safeCliDebugResponse,
13
17
  safeLegacyClaudeResponse,
@@ -38,13 +42,17 @@ cliDebugRouter.get('/cli/debug', (req, res) => {
38
42
  const model = optionalClaudeModel(req.query.model)
39
43
  const claudeConfig = getClaudeRunConfig()
40
44
  const codexConfig = getCodexRunConfig()
45
+ const cursorConfig = getCursorRunConfig()
41
46
  const claudeRuns = listClaudeRuns(limit, sessionId, model)
42
47
  const codexRuns = listCodexRuns(limit, sessionId)
48
+ const cursorRuns = listCursorRuns(limit, sessionId)
43
49
  res.json(safeCliDebugResponse(
44
50
  claudeConfig,
45
51
  claudeRuns[0],
46
52
  codexConfig,
47
53
  codexRuns[0],
54
+ cursorConfig,
55
+ cursorRuns[0],
48
56
  ))
49
57
  })
50
58