@gotcos/glasses-server 6.21.0 → 6.21.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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/server/lib/health-static-probes.ts +177 -0
- package/server/lib/provider-proof.ts +44 -15
- package/server/routes/health.ts +13 -89
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 6.21.2
|
|
2
|
+
|
|
3
|
+
- Cache Python, Claude, Codex, and Cursor process probes for 30 seconds so the
|
|
4
|
+
public health endpoint remains cheap under frequent phone diagnostics. Stale
|
|
5
|
+
static versions are served while one background refresh runs; live recovery,
|
|
6
|
+
transcription, TTS, maintenance, and request fields remain fresh.
|
|
7
|
+
- Parse Cursor's actual `CLI Version` line instead of reporting the About
|
|
8
|
+
heading. Authenticated model discovery continues to be the authoritative
|
|
9
|
+
Cursor readiness signal.
|
|
10
|
+
|
|
11
|
+
## 6.21.1
|
|
12
|
+
|
|
13
|
+
- Make the transactional Claude readiness proof explicitly use Haiku instead
|
|
14
|
+
of inheriting a user's heavyweight default model. The no-tool proof now has
|
|
15
|
+
a 45-second bound, while normal COS queries keep their selected models.
|
|
16
|
+
- Preserve timeout and cancellation reasons across the provider-process close
|
|
17
|
+
race. A timed-out proof now reports `provider proof timed out` instead of the
|
|
18
|
+
misleading `provider process exited before launch`.
|
|
19
|
+
|
|
1
20
|
## 6.21.0
|
|
2
21
|
|
|
3
22
|
Transcription quality is now a machine-owned, observable two-tier policy.
|
package/package.json
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { PYTHON_BIN } from './python-bridge.js'
|
|
3
|
+
import {
|
|
4
|
+
getCursorModelCatalog,
|
|
5
|
+
isCursorProviderReady,
|
|
6
|
+
resolveAgentBinary,
|
|
7
|
+
} from './cursor-model-catalog.js'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CACHE_TTL_MS = 30_000
|
|
10
|
+
const PROBE_TIMEOUT_MS = 5_000
|
|
11
|
+
|
|
12
|
+
export interface HealthStaticProbeSnapshot {
|
|
13
|
+
python: string
|
|
14
|
+
claude: string
|
|
15
|
+
codex: string
|
|
16
|
+
cursor: string
|
|
17
|
+
claudeAvailable: boolean
|
|
18
|
+
codexAvailable: boolean
|
|
19
|
+
cursorAvailable: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CachedProbe<T> {
|
|
23
|
+
value: T
|
|
24
|
+
refreshedAt: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Small stale-while-revalidate cache used for process-level health probes.
|
|
29
|
+
* Runtime fields (recordings, recovery leases, Whisper, TTS, request counts)
|
|
30
|
+
* remain fresh on every /api/health request.
|
|
31
|
+
*/
|
|
32
|
+
export function createStaticProbeCache<T>(
|
|
33
|
+
load: () => Promise<T>,
|
|
34
|
+
ttlMs: () => number,
|
|
35
|
+
now: () => number = Date.now,
|
|
36
|
+
): { get: () => Promise<T>; reset: () => void } {
|
|
37
|
+
let cached: CachedProbe<T> | null = null
|
|
38
|
+
let inFlight: Promise<T> | null = null
|
|
39
|
+
|
|
40
|
+
const refresh = (): Promise<T> => {
|
|
41
|
+
if (inFlight) return inFlight
|
|
42
|
+
inFlight = load().then((value) => {
|
|
43
|
+
cached = { value, refreshedAt: now() }
|
|
44
|
+
return value
|
|
45
|
+
}).finally(() => {
|
|
46
|
+
inFlight = null
|
|
47
|
+
})
|
|
48
|
+
return inFlight
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
get: async () => {
|
|
53
|
+
if (!cached) return refresh()
|
|
54
|
+
if (now() - cached.refreshedAt < ttlMs()) return cached.value
|
|
55
|
+
// A stale static version string is safer than making liveness wait for
|
|
56
|
+
// four child processes. Refresh it for the next request in background.
|
|
57
|
+
void refresh().catch(() => {})
|
|
58
|
+
return cached.value
|
|
59
|
+
},
|
|
60
|
+
reset: () => {
|
|
61
|
+
cached = null
|
|
62
|
+
inFlight = null
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cacheTtlMs(): number {
|
|
68
|
+
const raw = Number(process.env.COS_HEALTH_STATIC_PROBE_TTL_MS ?? DEFAULT_CACHE_TTL_MS)
|
|
69
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : DEFAULT_CACHE_TTL_MS
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function execute(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
execFile(file, args, {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
77
|
+
maxBuffer: 256 * 1024,
|
|
78
|
+
killSignal: 'SIGKILL',
|
|
79
|
+
}, (error, stdout, stderr) => {
|
|
80
|
+
if (error) return reject(error)
|
|
81
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) })
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseCursorAboutVersion(output: string): string | undefined {
|
|
87
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
88
|
+
const line = rawLine.trim()
|
|
89
|
+
const match = /^CLI Version\s*:?\s*(\d{4}(?:\.\d+){1,3}(?:[-+][a-z0-9.-]+)?)$/i.exec(line)
|
|
90
|
+
if (match) return match[1]
|
|
91
|
+
}
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function firstNonemptyLine(output: string): string | undefined {
|
|
96
|
+
return output.split(/\r?\n/).map(line => line.trim()).find(Boolean)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function probePython(): Promise<string> {
|
|
100
|
+
if (!PYTHON_BIN) return 'standalone'
|
|
101
|
+
try {
|
|
102
|
+
return firstNonemptyLine((await execute(PYTHON_BIN, ['--version'])).stdout) ?? 'available'
|
|
103
|
+
} catch {
|
|
104
|
+
return 'error'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function probeClaude(): Promise<{ value: string; available: boolean }> {
|
|
109
|
+
try {
|
|
110
|
+
const result = await execute('claude', ['--version'])
|
|
111
|
+
return { value: firstNonemptyLine(result.stdout) ?? 'available', available: true }
|
|
112
|
+
} catch {
|
|
113
|
+
return { value: 'error', available: false }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function probeCodex(): Promise<{ value: string; available: boolean }> {
|
|
118
|
+
try {
|
|
119
|
+
const result = await execute('codex', ['--version'])
|
|
120
|
+
const combined = `${result.stdout}\n${result.stderr}`.trim()
|
|
121
|
+
const versionLine = combined.split(/\r?\n/).map(line => line.trim())
|
|
122
|
+
.find(line => /^codex(?:-cli)?\s+/i.test(line))
|
|
123
|
+
return { value: versionLine ?? firstNonemptyLine(combined) ?? 'available', available: true }
|
|
124
|
+
} catch {
|
|
125
|
+
return { value: 'error', available: false }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function probeCursor(): Promise<{ value: string; available: boolean }> {
|
|
130
|
+
const agentBinary = resolveAgentBinary()
|
|
131
|
+
if (!agentBinary) return { value: 'error', available: false }
|
|
132
|
+
try {
|
|
133
|
+
const result = await execute(agentBinary, ['about'])
|
|
134
|
+
const combined = `${result.stdout}\n${result.stderr}`.trim()
|
|
135
|
+
const version = parseCursorAboutVersion(combined)
|
|
136
|
+
// Catalog discovery is authenticated downstream truth and can take up to
|
|
137
|
+
// seven seconds. Warm it without putting that latency on public health.
|
|
138
|
+
void getCursorModelCatalog().catch(() => {})
|
|
139
|
+
const available = isCursorProviderReady()
|
|
140
|
+
const value = version ?? 'available'
|
|
141
|
+
return {
|
|
142
|
+
value: available ? value : `${value} (models unresolved)`,
|
|
143
|
+
available,
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
return { value: 'error', available: false }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function loadStaticHealthProbes(): Promise<HealthStaticProbeSnapshot> {
|
|
151
|
+
const [python, claude, codex, cursor] = await Promise.all([
|
|
152
|
+
probePython(),
|
|
153
|
+
probeClaude(),
|
|
154
|
+
probeCodex(),
|
|
155
|
+
probeCursor(),
|
|
156
|
+
])
|
|
157
|
+
return {
|
|
158
|
+
python,
|
|
159
|
+
claude: claude.value,
|
|
160
|
+
codex: codex.value,
|
|
161
|
+
cursor: cursor.value,
|
|
162
|
+
claudeAvailable: claude.available,
|
|
163
|
+
codexAvailable: codex.available,
|
|
164
|
+
cursorAvailable: cursor.available,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const staticProbeCache = createStaticProbeCache(loadStaticHealthProbes, cacheTtlMs)
|
|
169
|
+
|
|
170
|
+
export function getHealthStaticProbes(): Promise<HealthStaticProbeSnapshot> {
|
|
171
|
+
return staticProbeCache.get()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Test hook. */
|
|
175
|
+
export function _resetHealthStaticProbeCache(): void {
|
|
176
|
+
staticProbeCache.reset()
|
|
177
|
+
}
|
|
@@ -17,7 +17,7 @@ const PROOF_PROMPT = `This is an automated local readiness check. Do not use too
|
|
|
17
17
|
const successCache = new Map<ProofProvider, ProviderProofResult>()
|
|
18
18
|
const inFlight = new Map<ProofProvider, Promise<ProviderProofResult>>()
|
|
19
19
|
|
|
20
|
-
interface ProcessResult {
|
|
20
|
+
export interface ProcessResult {
|
|
21
21
|
code: number | null
|
|
22
22
|
stdout: string
|
|
23
23
|
stderr: string
|
|
@@ -25,7 +25,9 @@ interface ProcessResult {
|
|
|
25
25
|
aborted: boolean
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
type TerminationReason = 'timeout' | 'abort' | null
|
|
29
|
+
|
|
30
|
+
export function runBounded(
|
|
29
31
|
command: string,
|
|
30
32
|
args: string[],
|
|
31
33
|
input: string,
|
|
@@ -48,6 +50,11 @@ function runBounded(
|
|
|
48
50
|
let stdout = ''
|
|
49
51
|
let stderr = ''
|
|
50
52
|
let settled = false
|
|
53
|
+
// The child `close` event normally wins the race against the async
|
|
54
|
+
// terminateProviderProcess() result. Record WHY termination began before
|
|
55
|
+
// sending a signal so that close cannot misreport a timeout as
|
|
56
|
+
// "exited before launch" (Control 0.3.5 field failure, 2026-08-03).
|
|
57
|
+
let terminationReason: TerminationReason = null
|
|
51
58
|
const cap = (value: string) => value.slice(-256_000)
|
|
52
59
|
child.stdout.on('data', chunk => { stdout = cap(stdout + chunk.toString()) })
|
|
53
60
|
child.stderr.on('data', chunk => { stderr = cap(stderr + chunk.toString()) })
|
|
@@ -58,27 +65,57 @@ function runBounded(
|
|
|
58
65
|
signal?.removeEventListener('abort', abort)
|
|
59
66
|
resolvePromise(result)
|
|
60
67
|
}
|
|
68
|
+
const terminatedResult = (code: number | null): ProcessResult => ({
|
|
69
|
+
code,
|
|
70
|
+
stdout,
|
|
71
|
+
stderr,
|
|
72
|
+
timedOut: terminationReason === 'timeout',
|
|
73
|
+
aborted: terminationReason === 'abort',
|
|
74
|
+
})
|
|
61
75
|
const timer = setTimeout(() => {
|
|
76
|
+
if (settled || terminationReason) return
|
|
77
|
+
terminationReason = 'timeout'
|
|
62
78
|
void terminateProviderProcess(child, { termGraceMs: 50 }).then(result => {
|
|
63
|
-
if (result.closed) finish(
|
|
79
|
+
if (result.closed) finish(terminatedResult(result.code))
|
|
64
80
|
else console.error('[provider-proof] timed-out provider did not close after SIGKILL; retaining request ownership')
|
|
65
81
|
})
|
|
66
82
|
}, timeoutMs)
|
|
67
83
|
timer.unref?.()
|
|
68
84
|
const abort = () => {
|
|
85
|
+
if (settled || terminationReason) return
|
|
86
|
+
terminationReason = 'abort'
|
|
69
87
|
void terminateProviderProcess(child).then(result => {
|
|
70
|
-
if (result.closed) finish(
|
|
88
|
+
if (result.closed) finish(terminatedResult(result.code))
|
|
71
89
|
else console.error('[provider-proof] canceled provider did not close after SIGKILL; retaining request ownership')
|
|
72
90
|
})
|
|
73
91
|
}
|
|
74
|
-
child.once('error', err =>
|
|
75
|
-
|
|
92
|
+
child.once('error', err => {
|
|
93
|
+
stderr = cap(stderr + err.message)
|
|
94
|
+
finish(terminatedResult(null))
|
|
95
|
+
})
|
|
96
|
+
child.once('close', code => finish(terminatedResult(code)))
|
|
76
97
|
signal?.addEventListener('abort', abort, { once: true })
|
|
77
98
|
child.stdin.on('error', () => { /* close/error is authoritative */ })
|
|
78
99
|
child.stdin.end(input)
|
|
79
100
|
})
|
|
80
101
|
}
|
|
81
102
|
|
|
103
|
+
export const CLAUDE_PROOF_MODEL = 'haiku'
|
|
104
|
+
export const CLAUDE_PROOF_TIMEOUT_MS = 45_000
|
|
105
|
+
|
|
106
|
+
export function claudeProofArgs(): string[] {
|
|
107
|
+
return [
|
|
108
|
+
'-p',
|
|
109
|
+
'--model', CLAUDE_PROOF_MODEL,
|
|
110
|
+
'--output-format', 'json',
|
|
111
|
+
'--permission-mode', 'dontAsk',
|
|
112
|
+
'--tools', '',
|
|
113
|
+
'--allowedTools', '',
|
|
114
|
+
'--system-prompt', PROOF_PROMPT,
|
|
115
|
+
PROOF_PROMPT,
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
export function claudeProofText(stdout: string): string {
|
|
83
120
|
try {
|
|
84
121
|
const parsed = JSON.parse(stdout) as { result?: unknown }
|
|
@@ -120,15 +157,7 @@ function safeProofError(result: ProcessResult): string {
|
|
|
120
157
|
async function executeProof(provider: ProofProvider, signal?: AbortSignal): Promise<ProviderProofResult> {
|
|
121
158
|
const started = Date.now()
|
|
122
159
|
const result = provider === 'claude'
|
|
123
|
-
? await runBounded('claude',
|
|
124
|
-
'-p',
|
|
125
|
-
'--output-format', 'json',
|
|
126
|
-
'--permission-mode', 'dontAsk',
|
|
127
|
-
'--tools', '',
|
|
128
|
-
'--allowedTools', '',
|
|
129
|
-
'--system-prompt', PROOF_PROMPT,
|
|
130
|
-
PROOF_PROMPT,
|
|
131
|
-
], '', 120_000, signal)
|
|
160
|
+
? await runBounded('claude', claudeProofArgs(), '', CLAUDE_PROOF_TIMEOUT_MS, signal)
|
|
132
161
|
: await runBounded('codex', [
|
|
133
162
|
'exec',
|
|
134
163
|
'--sandbox', 'read-only',
|
package/server/routes/health.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express'
|
|
2
|
-
import { execFile } from 'node:child_process'
|
|
3
2
|
import { statSync } from 'node:fs'
|
|
4
3
|
import { resolve } from 'node:path'
|
|
5
|
-
import { COS_SCRIPTS_DIR, COS_MODE
|
|
4
|
+
import { COS_SCRIPTS_DIR, COS_MODE } from '../lib/python-bridge.js'
|
|
6
5
|
import { serverMetrics } from '../lib/server-metrics.js'
|
|
7
6
|
import { getServerInstanceId } from '../lib/server-instance-id.js'
|
|
8
7
|
import { localFirstMeetingsCapability } from '../lib/local-first-meetings-contract.js'
|
|
@@ -24,7 +23,6 @@ import {
|
|
|
24
23
|
getCursorModelCatalog,
|
|
25
24
|
getCursorModelCatalogSnapshot,
|
|
26
25
|
isCursorProviderReady,
|
|
27
|
-
resolveAgentBinary,
|
|
28
26
|
} from '../lib/cursor-model-catalog.js'
|
|
29
27
|
import { isMediaProcessingReady } from '../lib/image-safety.js'
|
|
30
28
|
import { G2_LENS_VARIANT_CAPABILITY } from '../lib/media-store.js'
|
|
@@ -41,6 +39,7 @@ import { getMeetingSyncSnapshot } from '../lib/meeting-batch-progress.js'
|
|
|
41
39
|
import { listUnsavedCaptures } from '../lib/unsaved-audio-quarantine.js'
|
|
42
40
|
import { getWhisperPreviewCapability } from '../lib/whisper-preview.js'
|
|
43
41
|
import { getTranscriptionProfileStatus } from '../lib/profile.js'
|
|
42
|
+
import { getHealthStaticProbes } from '../lib/health-static-probes.js'
|
|
44
43
|
|
|
45
44
|
export const healthRouter = Router()
|
|
46
45
|
|
|
@@ -74,101 +73,26 @@ function durableQueryJobStatus() {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
healthRouter.get('/health', async (_req, res) => {
|
|
77
|
-
await
|
|
76
|
+
const [, staticProbes] = await Promise.all([
|
|
77
|
+
refreshLocalTtsHealth(),
|
|
78
|
+
getHealthStaticProbes(),
|
|
79
|
+
])
|
|
78
80
|
const checks: Record<string, string | number | boolean> = {
|
|
79
81
|
status: 'ok',
|
|
80
82
|
mode: COS_MODE ? 'cos' : 'standalone',
|
|
81
83
|
server: 'ok',
|
|
82
|
-
python:
|
|
83
|
-
claude:
|
|
84
|
-
codex:
|
|
85
|
-
cursor:
|
|
84
|
+
python: staticProbes.python,
|
|
85
|
+
claude: staticProbes.claude,
|
|
86
|
+
codex: staticProbes.codex,
|
|
87
|
+
cursor: staticProbes.cursor,
|
|
86
88
|
uptime_seconds: Math.floor((Date.now() - serverMetrics.startedAt) / 1000),
|
|
87
89
|
request_count: serverMetrics.requestCount,
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
// Feature detection flags
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Check Python venv (COS mode only)
|
|
96
|
-
if (PYTHON_BIN) {
|
|
97
|
-
try {
|
|
98
|
-
await new Promise<void>((resolve, reject) => {
|
|
99
|
-
execFile(PYTHON_BIN!, ['--version'], { timeout: 5000 }, (err, stdout) => {
|
|
100
|
-
if (err) return reject(err)
|
|
101
|
-
checks.python = stdout.trim()
|
|
102
|
-
resolve()
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
} catch {
|
|
106
|
-
checks.python = 'error'
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
checks.python = 'standalone'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check claude CLI
|
|
113
|
-
try {
|
|
114
|
-
await new Promise<void>((resolve, reject) => {
|
|
115
|
-
execFile('claude', ['--version'], { timeout: 5000 }, (err, stdout) => {
|
|
116
|
-
if (err) return reject(err)
|
|
117
|
-
checks.claude = stdout.trim()
|
|
118
|
-
claudeAvailable = true
|
|
119
|
-
resolve()
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
} catch {
|
|
123
|
-
checks.claude = 'error'
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check Codex CLI. The desktop CLI can print benign PATH warnings to stderr,
|
|
127
|
-
// so version extraction uses stdout + stderr and looks for the codex-cli line.
|
|
128
|
-
try {
|
|
129
|
-
await new Promise<void>((resolve, reject) => {
|
|
130
|
-
execFile('codex', ['--version'], { timeout: 5000 }, (err, stdout, stderr) => {
|
|
131
|
-
if (err) return reject(err)
|
|
132
|
-
const combined = `${stdout}\n${stderr}`.trim()
|
|
133
|
-
const versionLine = combined.split('\n').map(line => line.trim()).find(line => /^codex(?:-cli)?\s+/i.test(line))
|
|
134
|
-
checks.codex = versionLine ?? combined.split('\n')[0] ?? 'available'
|
|
135
|
-
codexAvailable = true
|
|
136
|
-
resolve()
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
} catch {
|
|
140
|
-
checks.codex = 'error'
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Cursor Agent CLI — probe via `agent about` only (≤5s). Never use
|
|
144
|
-
// `agent status` (can hang on login UX while logged out).
|
|
145
|
-
try {
|
|
146
|
-
const agentBinary = resolveAgentBinary()
|
|
147
|
-
if (!agentBinary) {
|
|
148
|
-
checks.cursor = 'error'
|
|
149
|
-
} else {
|
|
150
|
-
await new Promise<void>((resolveCheck, reject) => {
|
|
151
|
-
execFile(agentBinary, ['about'], { timeout: 5000 }, (err, stdout, stderr) => {
|
|
152
|
-
if (err) return reject(err)
|
|
153
|
-
const combined = `${stdout}\n${stderr}`.trim()
|
|
154
|
-
const versionLine = combined.split('\n').map(line => line.trim()).find(line =>
|
|
155
|
-
/CLI Version|cursor|agent/i.test(line),
|
|
156
|
-
)
|
|
157
|
-
checks.cursor = versionLine ?? combined.split('\n')[0] ?? 'available'
|
|
158
|
-
resolveCheck()
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
await getCursorModelCatalog()
|
|
162
|
-
cursorAvailable = isCursorProviderReady()
|
|
163
|
-
if (!cursorAvailable) {
|
|
164
|
-
checks.cursor = typeof checks.cursor === 'string' && checks.cursor !== 'error'
|
|
165
|
-
? `${checks.cursor} (models unresolved)`
|
|
166
|
-
: 'error'
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
checks.cursor = 'error'
|
|
171
|
-
}
|
|
93
|
+
const claudeAvailable = staticProbes.claudeAvailable
|
|
94
|
+
const codexAvailable = staticProbes.codexAvailable
|
|
95
|
+
const cursorAvailable = staticProbes.cursorAvailable
|
|
172
96
|
|
|
173
97
|
// Check session cache freshness (COS mode only)
|
|
174
98
|
if (COS_SCRIPTS_DIR) {
|