@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.
- package/.env.example +7 -0
- package/CHANGELOG.md +19 -0
- package/README.md +29 -9
- package/bin/cli.cjs +71 -5
- package/package.json +8 -3
- package/server/lib/claude-bridge.ts +14 -1
- package/server/lib/cli-debug-view.ts +31 -4
- package/server/lib/cos-operations-meetings.ts +365 -0
- package/server/lib/cursor-bridge.ts +726 -0
- package/server/lib/cursor-engine-sessions.ts +162 -0
- package/server/lib/cursor-model-catalog.ts +288 -0
- package/server/lib/cursor-run-ledger.ts +300 -0
- package/server/lib/hallucination-filter.ts +1 -1
- package/server/lib/model-router.ts +26 -1
- package/server/lib/provider-terminal-error.ts +1 -1
- package/server/lib/query-job-coordinator.ts +1 -0
- package/server/lib/query-job-runtime.ts +8 -2
- package/server/lib/query-job-store.ts +11 -3
- package/server/lib/query-job-types.ts +2 -1
- package/server/lib/speaker-trainer.ts +1 -1
- package/server/lib/token-audit.ts +11 -1
- package/server/models/silero_vad.onnx +0 -0
- package/server/routes/cli-debug.ts +8 -0
- package/server/routes/health.ts +56 -4
- package/server/routes/meetings.ts +43 -3
- package/server/routes/openai-compat.ts +28 -4
- package/shared/model-preference.ts +50 -2
|
@@ -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
|
-
//
|
|
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 {
|
|
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
|
-
:
|
|
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'
|
|
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'
|
|
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
|
|
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
|
|