@gotcos/glasses-server 6.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/.cos-profile.example.json +7 -0
- package/.env.example +44 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/bin/cli.cjs +203 -0
- package/package.json +53 -0
- package/server/env.ts +26 -0
- package/server/index.ts +211 -0
- package/server/lib/archive-budget.ts +65 -0
- package/server/lib/archive.ts +414 -0
- package/server/lib/atomic-fs.ts +50 -0
- package/server/lib/audio-enhance.ts +87 -0
- package/server/lib/claude-bridge.ts +682 -0
- package/server/lib/claude-circuit.ts +52 -0
- package/server/lib/claude-run-ledger.ts +279 -0
- package/server/lib/codex-bridge.ts +476 -0
- package/server/lib/codex-engine-sessions.ts +140 -0
- package/server/lib/codex-run-ledger.ts +298 -0
- package/server/lib/context-builder.ts +210 -0
- package/server/lib/conversation.ts +587 -0
- package/server/lib/data-dir.ts +20 -0
- package/server/lib/display-bus.ts +21 -0
- package/server/lib/display-format.ts +23 -0
- package/server/lib/fuzzy-correct.ts +286 -0
- package/server/lib/hallucination-filter.ts +469 -0
- package/server/lib/local-day.ts +13 -0
- package/server/lib/model-router.ts +38 -0
- package/server/lib/openai-key.ts +155 -0
- package/server/lib/openai-whisper-budget.ts +170 -0
- package/server/lib/profile.ts +94 -0
- package/server/lib/python-bridge.ts +84 -0
- package/server/lib/response-cache.ts +138 -0
- package/server/lib/session-cache-writer.ts +266 -0
- package/server/lib/session-log.ts +162 -0
- package/server/lib/speaker-embeddings.ts +578 -0
- package/server/lib/telegram-notify.ts +85 -0
- package/server/lib/token-audit.ts +50 -0
- package/server/lib/transcribe-audio.ts +187 -0
- package/server/lib/utils.ts +5 -0
- package/server/lib/vad-silero.ts +179 -0
- package/server/lib/whisper-local.ts +697 -0
- package/server/routes/diag.ts +115 -0
- package/server/routes/display.ts +65 -0
- package/server/routes/health.ts +128 -0
- package/server/routes/openai-compat.ts +446 -0
- package/server/routes/openai-key.ts +121 -0
- package/server/routes/query.ts +121 -0
- package/server/routes/transcribe-stream.ts +1090 -0
- package/server/routes/transcribe.ts +55 -0
- package/shared/model-preference.ts +81 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Reusable circuit breaker for `claude -p` callers. After `maxFailures`
|
|
2
|
+
// consecutive failures the breaker OPENS for `cooldownMs`, then HALF-OPENS
|
|
3
|
+
// (allows one trial call); a success CLOSES it. Mirrors the inline breaker in
|
|
4
|
+
// routes/meeting.ts:38-71, but as a standalone instance so independent callers
|
|
5
|
+
// (e.g. the outbound-dictation auto-clean) keep SEPARATE failure accounting —
|
|
6
|
+
// a meeting-correction failure must not open the dictation breaker, and vice
|
|
7
|
+
// versa.
|
|
8
|
+
|
|
9
|
+
export interface ClaudeBreaker {
|
|
10
|
+
/** True when the breaker is OPEN — skip the call. */
|
|
11
|
+
isOpen(): boolean
|
|
12
|
+
recordFailure(): void
|
|
13
|
+
recordSuccess(): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createBreaker(opts: {
|
|
17
|
+
label: string
|
|
18
|
+
maxFailures?: number
|
|
19
|
+
cooldownMs?: number
|
|
20
|
+
}): ClaudeBreaker {
|
|
21
|
+
const maxFailures = opts.maxFailures ?? 2
|
|
22
|
+
const cooldownMs = opts.cooldownMs ?? 30 * 60 * 1000 // 30 minutes
|
|
23
|
+
let consecutiveFailures = 0
|
|
24
|
+
let openedAt = 0
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
isOpen(): boolean {
|
|
28
|
+
if (consecutiveFailures < maxFailures) return false
|
|
29
|
+
const elapsed = Date.now() - openedAt
|
|
30
|
+
if (elapsed >= cooldownMs) {
|
|
31
|
+
// Half-open: allow one attempt to see if it recovered.
|
|
32
|
+
console.log(`[${opts.label}] circuit HALF-OPEN — cooldown elapsed (${(elapsed / 60000).toFixed(0)}min), trying one call`)
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
return true
|
|
36
|
+
},
|
|
37
|
+
recordFailure(): void {
|
|
38
|
+
consecutiveFailures++
|
|
39
|
+
if (consecutiveFailures >= maxFailures) {
|
|
40
|
+
openedAt = Date.now()
|
|
41
|
+
console.error(`[${opts.label}] ⚠ circuit OPEN — ${consecutiveFailures} consecutive failures. Retry in ${(cooldownMs / 60000).toFixed(0)}min`)
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
recordSuccess(): void {
|
|
45
|
+
if (consecutiveFailures > 0) {
|
|
46
|
+
console.log(`[${opts.label}] circuit CLOSED — recovered after ${consecutiveFailures} failure(s)`)
|
|
47
|
+
consecutiveFailures = 0
|
|
48
|
+
openedAt = 0
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
5
|
+
import { dataPath } from './data-dir.js'
|
|
6
|
+
import type { ClaudeModelPreference } from '../../shared/model-preference.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_RUNS = 100
|
|
9
|
+
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60_000
|
|
10
|
+
const ERROR_PREVIEW_CHARS = 160
|
|
11
|
+
const RUNNING_STALE_MS = 30 * 60_000
|
|
12
|
+
export const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'] as const
|
|
13
|
+
export type ClaudeEffortLevel = typeof CLAUDE_EFFORT_LEVELS[number]
|
|
14
|
+
export const DEFAULT_CLAUDE_EFFORT_LEVEL: ClaudeEffortLevel = 'high'
|
|
15
|
+
|
|
16
|
+
function getProcessStartedAtMs(): number {
|
|
17
|
+
return Date.now() - Math.floor(process.uptime() * 1000)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ClaudeRunStatus =
|
|
21
|
+
| 'running'
|
|
22
|
+
| 'completed'
|
|
23
|
+
| 'failed'
|
|
24
|
+
| 'cancelled'
|
|
25
|
+
| 'client_disconnected'
|
|
26
|
+
|
|
27
|
+
export interface ClaudeRunRecord {
|
|
28
|
+
runId: string
|
|
29
|
+
cosSessionId: string
|
|
30
|
+
cliSessionId?: string
|
|
31
|
+
status: ClaudeRunStatus
|
|
32
|
+
createdAt: string
|
|
33
|
+
updatedAt: string
|
|
34
|
+
model: ClaudeModelPreference
|
|
35
|
+
cliCommand: string
|
|
36
|
+
effortLevel: ClaudeEffortLevel
|
|
37
|
+
cwd: string
|
|
38
|
+
resumed: boolean
|
|
39
|
+
trustMode: 'full-access'
|
|
40
|
+
timeoutMs: number
|
|
41
|
+
wallMaxMs: number
|
|
42
|
+
queryPreview?: string
|
|
43
|
+
outputPreview?: string
|
|
44
|
+
errorCode?: string
|
|
45
|
+
errorPreview?: string
|
|
46
|
+
durationMs?: number
|
|
47
|
+
exitCode?: number | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ClaudeRunEvent {
|
|
51
|
+
runId: string
|
|
52
|
+
ts: string
|
|
53
|
+
patch: Partial<ClaudeRunRecord>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ClaudeRunConfig {
|
|
57
|
+
cliCommand: string
|
|
58
|
+
persistenceEnabled: boolean
|
|
59
|
+
cwd: string
|
|
60
|
+
trustMode: 'full-access'
|
|
61
|
+
defaultEffortLevel: ClaudeEffortLevel
|
|
62
|
+
historyLimit: number
|
|
63
|
+
historyTtlDays: number
|
|
64
|
+
contentPreviewsEnabled: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function areClaudeContentPreviewsEnabled(): boolean {
|
|
68
|
+
return process.env.COS_CLAUDE_RUN_CONTENT_PREVIEWS === '1'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getClaudeExecutionCwd(): string {
|
|
72
|
+
return resolve(COS_SCRIPTS_DIR ?? process.cwd())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getClaudeEffortLevel(): ClaudeEffortLevel {
|
|
76
|
+
const raw = process.env.COS_CLAUDE_EFFORT_LEVEL?.trim()
|
|
77
|
+
if (!raw) return DEFAULT_CLAUDE_EFFORT_LEVEL
|
|
78
|
+
if ((CLAUDE_EFFORT_LEVELS as readonly string[]).includes(raw)) {
|
|
79
|
+
return raw as ClaudeEffortLevel
|
|
80
|
+
}
|
|
81
|
+
console.warn(
|
|
82
|
+
`[claude-run-ledger] Invalid COS_CLAUDE_EFFORT_LEVEL="${raw}"; ` +
|
|
83
|
+
`using ${DEFAULT_CLAUDE_EFFORT_LEVEL}. Valid: ${CLAUDE_EFFORT_LEVELS.join(', ')}`,
|
|
84
|
+
)
|
|
85
|
+
return DEFAULT_CLAUDE_EFFORT_LEVEL
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getClaudeRunConfig(): ClaudeRunConfig {
|
|
89
|
+
return {
|
|
90
|
+
cliCommand: 'claude -p',
|
|
91
|
+
persistenceEnabled: true,
|
|
92
|
+
cwd: getClaudeExecutionCwd(),
|
|
93
|
+
trustMode: 'full-access',
|
|
94
|
+
defaultEffortLevel: getClaudeEffortLevel(),
|
|
95
|
+
historyLimit: getMaxRuns(),
|
|
96
|
+
historyTtlDays: Math.round(getTtlMs() / (24 * 60 * 60_000)),
|
|
97
|
+
contentPreviewsEnabled: areClaudeContentPreviewsEnabled(),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getClaudeLedgerPath(): string {
|
|
102
|
+
return resolve(process.env.COS_CLAUDE_RUN_LEDGER_FILE || dataPath('claude-runs.jsonl'))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getMaxRuns(): number {
|
|
106
|
+
const raw = Number(process.env.COS_CLAUDE_RUN_LEDGER_MAX ?? DEFAULT_MAX_RUNS)
|
|
107
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_RUNS
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getTtlMs(): number {
|
|
111
|
+
const rawDays = Number(process.env.COS_CLAUDE_RUN_LEDGER_TTL_DAYS ?? 7)
|
|
112
|
+
return Number.isFinite(rawDays) && rawDays > 0 ? rawDays * 24 * 60 * 60_000 : DEFAULT_TTL_MS
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function appendEvent(event: ClaudeRunEvent): void {
|
|
116
|
+
try {
|
|
117
|
+
const path = getClaudeLedgerPath()
|
|
118
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
119
|
+
appendFileSync(path, JSON.stringify(event) + '\n')
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn('[claude-run-ledger] write skipped:', err)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readEvents(): ClaudeRunEvent[] {
|
|
126
|
+
const path = getClaudeLedgerPath()
|
|
127
|
+
if (!existsSync(path)) return []
|
|
128
|
+
try {
|
|
129
|
+
const events: ClaudeRunEvent[] = []
|
|
130
|
+
for (const line of readFileSync(path, 'utf-8')
|
|
131
|
+
.split('\n')
|
|
132
|
+
.map(line => line.trim())
|
|
133
|
+
.filter(Boolean)) {
|
|
134
|
+
try {
|
|
135
|
+
const event = JSON.parse(line) as ClaudeRunEvent
|
|
136
|
+
if (typeof event.runId === 'string' && typeof event.ts === 'string' && typeof event.patch === 'object') {
|
|
137
|
+
events.push(event)
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Skip torn/corrupt JSONL rows; valid prior records should stay visible.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return events
|
|
144
|
+
} catch {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hydrateRuns(): ClaudeRunRecord[] {
|
|
150
|
+
const runs = new Map<string, ClaudeRunRecord>()
|
|
151
|
+
const order = new Map<string, number>()
|
|
152
|
+
let eventIndex = 0
|
|
153
|
+
for (const event of readEvents()) {
|
|
154
|
+
eventIndex += 1
|
|
155
|
+
const existing = runs.get(event.runId)
|
|
156
|
+
const next = { ...(existing ?? {}), ...event.patch, runId: event.runId } as ClaudeRunRecord
|
|
157
|
+
runs.set(event.runId, next)
|
|
158
|
+
order.set(event.runId, eventIndex)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const cutoff = Date.now() - getTtlMs()
|
|
162
|
+
return Array.from(runs.values())
|
|
163
|
+
.filter(run => run.createdAt && Date.parse(run.updatedAt || run.createdAt) >= cutoff)
|
|
164
|
+
.map(run => {
|
|
165
|
+
const updatedMs = Date.parse(run.updatedAt || run.createdAt)
|
|
166
|
+
const predatesCurrentProcess = updatedMs < getProcessStartedAtMs() - 1000
|
|
167
|
+
if (run.status === 'running' && (predatesCurrentProcess || Date.now() - updatedMs > RUNNING_STALE_MS)) {
|
|
168
|
+
return {
|
|
169
|
+
...run,
|
|
170
|
+
status: 'client_disconnected' as ClaudeRunStatus,
|
|
171
|
+
errorCode: run.errorCode ?? 'claude.interrupted',
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return run
|
|
175
|
+
})
|
|
176
|
+
.sort((a, b) => {
|
|
177
|
+
const byCreated = Date.parse(b.createdAt) - Date.parse(a.createdAt)
|
|
178
|
+
if (byCreated !== 0) return byCreated
|
|
179
|
+
return (order.get(b.runId) ?? 0) - (order.get(a.runId) ?? 0)
|
|
180
|
+
})
|
|
181
|
+
.slice(0, getMaxRuns())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function redactForClaudeLedger(value: string, maxChars = ERROR_PREVIEW_CHARS): string {
|
|
185
|
+
return value
|
|
186
|
+
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[email]')
|
|
187
|
+
.replace(/\b(?:sk|sess|ghp|github_pat|glpat)-[A-Za-z0-9_\-]{12,}\b/g, '[token]')
|
|
188
|
+
.replace(/\bBearer\s+[A-Za-z0-9._\-]{12,}\b/gi, 'Bearer [token]')
|
|
189
|
+
.replace(/[A-Za-z0-9+/=]{80,}/g, '[blob]')
|
|
190
|
+
.replace(/\s+/g, ' ')
|
|
191
|
+
.trim()
|
|
192
|
+
.slice(0, maxChars)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function classifyClaudeError(message: string): string {
|
|
196
|
+
const text = message.toLowerCase()
|
|
197
|
+
if (/command not found|enoent|not found/.test(text)) return 'claude.cli_unavailable'
|
|
198
|
+
if (/permission|denied|sandbox|read-only|operation not permitted/.test(text)) return 'claude.permission_denied'
|
|
199
|
+
if (/auth|login|sign in|unauthorized|forbidden|token/.test(text)) return 'claude.auth_error'
|
|
200
|
+
if (/timeout|timed out|wall clock|no output/.test(text)) return 'claude.timeout'
|
|
201
|
+
if (/exit\s+\d+/.test(text)) return 'claude.nonzero_exit'
|
|
202
|
+
return 'claude.error'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function startClaudeRun(input: {
|
|
206
|
+
cosSessionId: string
|
|
207
|
+
model: ClaudeModelPreference
|
|
208
|
+
cwd: string
|
|
209
|
+
resumed: boolean
|
|
210
|
+
cliSessionId?: string
|
|
211
|
+
timeoutMs: number
|
|
212
|
+
wallMaxMs: number
|
|
213
|
+
query: string
|
|
214
|
+
}): ClaudeRunRecord {
|
|
215
|
+
const now = new Date().toISOString()
|
|
216
|
+
const run: ClaudeRunRecord = {
|
|
217
|
+
runId: `claude-${crypto.randomUUID().slice(0, 8)}`,
|
|
218
|
+
cosSessionId: input.cosSessionId,
|
|
219
|
+
cliSessionId: input.cliSessionId,
|
|
220
|
+
status: 'running',
|
|
221
|
+
createdAt: now,
|
|
222
|
+
updatedAt: now,
|
|
223
|
+
model: input.model,
|
|
224
|
+
cliCommand: 'claude -p',
|
|
225
|
+
effortLevel: getClaudeEffortLevel(),
|
|
226
|
+
cwd: input.cwd,
|
|
227
|
+
resumed: input.resumed,
|
|
228
|
+
trustMode: 'full-access',
|
|
229
|
+
timeoutMs: input.timeoutMs,
|
|
230
|
+
wallMaxMs: input.wallMaxMs,
|
|
231
|
+
}
|
|
232
|
+
if (areClaudeContentPreviewsEnabled()) {
|
|
233
|
+
run.queryPreview = redactForClaudeLedger(input.query)
|
|
234
|
+
}
|
|
235
|
+
appendEvent({ runId: run.runId, ts: now, patch: run })
|
|
236
|
+
return run
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function updateClaudeRun(runId: string, patch: Partial<Omit<ClaudeRunRecord, 'runId' | 'createdAt'>>): ClaudeRunRecord | null {
|
|
240
|
+
const ts = new Date().toISOString()
|
|
241
|
+
appendEvent({ runId, ts, patch: { ...patch, updatedAt: ts } })
|
|
242
|
+
return getClaudeRun(runId)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function finishClaudeRun(runId: string, input: {
|
|
246
|
+
status: Exclude<ClaudeRunStatus, 'running'>
|
|
247
|
+
startedAtMs: number
|
|
248
|
+
output?: string
|
|
249
|
+
error?: string
|
|
250
|
+
exitCode?: number | null
|
|
251
|
+
}): ClaudeRunRecord | null {
|
|
252
|
+
const patch: Partial<ClaudeRunRecord> = {
|
|
253
|
+
status: input.status,
|
|
254
|
+
durationMs: Math.max(0, Date.now() - input.startedAtMs),
|
|
255
|
+
exitCode: input.exitCode,
|
|
256
|
+
}
|
|
257
|
+
if (input.output && areClaudeContentPreviewsEnabled()) {
|
|
258
|
+
patch.outputPreview = redactForClaudeLedger(input.output)
|
|
259
|
+
}
|
|
260
|
+
if (input.error) {
|
|
261
|
+
patch.errorCode = classifyClaudeError(input.error)
|
|
262
|
+
if (areClaudeContentPreviewsEnabled()) {
|
|
263
|
+
patch.errorPreview = redactForClaudeLedger(input.error)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return updateClaudeRun(runId, patch)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function listClaudeRuns(limit = 20, cosSessionId?: string, model?: ClaudeModelPreference): ClaudeRunRecord[] {
|
|
270
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(Math.floor(limit), getMaxRuns()) : 20
|
|
271
|
+
return hydrateRuns()
|
|
272
|
+
.filter(run => !cosSessionId || run.cosSessionId === cosSessionId)
|
|
273
|
+
.filter(run => !model || run.model === model)
|
|
274
|
+
.slice(0, safeLimit)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getClaudeRun(runId: string): ClaudeRunRecord | null {
|
|
278
|
+
return hydrateRuns().find(run => run.runId === runId) ?? null
|
|
279
|
+
}
|