@gotcos/glasses-server 6.12.7 → 6.13.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/CHANGELOG.md +7 -0
- package/README.md +10 -0
- package/bin/cli.cjs +12 -0
- package/bin/managed-server.cjs +28 -0
- package/managed-runtime-contract.json +23 -0
- package/package.json +5 -2
- package/server/index.ts +92 -35
- package/server/lib/launch-dir.ts +13 -7
- package/server/lib/maintenance-lifecycle.ts +735 -0
- package/server/lib/managed-runtime.ts +44 -0
- package/server/lib/query-job-coordinator.ts +36 -4
- package/server/lib/query-job-runtime.ts +38 -26
- package/server/routes/health.ts +15 -12
- package/server/routes/maintenance.ts +160 -0
- package/server/routes/meeting.ts +25 -8
- package/server/routes/openai-compat.ts +82 -36
- package/server/routes/prompt-drafts.ts +51 -8
- package/server/routes/query.ts +52 -24
- package/server/routes/transcribe-stream.ts +49 -3
- package/server/routes/transcribe.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -500,3 +500,10 @@ it directly, with no second repository to clone.
|
|
|
500
500
|
can reach the server over your mesh/LAN. The IP allowlist blocks public traffic.
|
|
501
501
|
- **Persistent config** at `~/.cos-glasses/.env`.
|
|
502
502
|
- Requires Node.js 20.11+.
|
|
503
|
+
# 6.13.0
|
|
504
|
+
|
|
505
|
+
- Added a non-interactive managed-server entrypoint for the COS Control macOS app.
|
|
506
|
+
- Added authenticated maintenance status and guarded local Whisper restart contracts.
|
|
507
|
+
- Added `--prepare-only` to the existing guided launcher so first-run dependencies can be prepared without leaving a second server process running.
|
|
508
|
+
- Added a provider-neutral managed work-folder setting while preserving the existing interactive launch-directory behavior.
|
|
509
|
+
- Kept the existing `npx @gotcos/glasses-server` foreground workflow fully compatible.
|
package/README.md
CHANGED
|
@@ -11,6 +11,16 @@ API key is pasted into the phone for chat.
|
|
|
11
11
|
npx --yes @gotcos/glasses-server@latest
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
For the optional COS Control macOS menu bar app, prepare dependencies without
|
|
15
|
+
leaving a foreground server running:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx --yes @gotcos/glasses-server@latest --prepare-only
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
COS Control then installs the same npm package as a launchd-managed runtime.
|
|
22
|
+
The original foreground command remains supported and unchanged.
|
|
23
|
+
|
|
14
24
|
The launcher checks Node, finds your CLI, checks voice and image processing,
|
|
15
25
|
downloads the local voice model when needed, writes `~/.cos-glasses/.env`, and
|
|
16
26
|
starts the server on `0.0.0.0:3141`. On boot it prints
|
package/bin/cli.cjs
CHANGED
|
@@ -41,6 +41,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
41
41
|
console.log('')
|
|
42
42
|
console.log(' Usage:')
|
|
43
43
|
console.log(' npx --yes @gotcos/glasses-server@latest')
|
|
44
|
+
console.log(' npx --yes @gotcos/glasses-server@latest --prepare-only')
|
|
44
45
|
console.log('')
|
|
45
46
|
console.log(' Requirements:')
|
|
46
47
|
console.log(' - Node.js 20.11+')
|
|
@@ -174,6 +175,17 @@ try {
|
|
|
174
175
|
process.exit(1)
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
// Controller probes are deliberately read-only. They verify Node, agent auth,
|
|
179
|
+
// and the packaged runtime without creating config, downloading models,
|
|
180
|
+
// changing permissions, or starting a listener.
|
|
181
|
+
if (process.argv.includes('--prepare-only')) {
|
|
182
|
+
console.log('')
|
|
183
|
+
console.log(green(' ✓ Non-mutating readiness check complete'))
|
|
184
|
+
console.log(' COS Control can perform guided installation without hidden setup side effects.')
|
|
185
|
+
console.log('')
|
|
186
|
+
process.exit(0)
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
// Step 4: persistent config at ~/.cos-glasses/ (survives npx cache churn)
|
|
178
190
|
function securePrivateDirectory(dir) {
|
|
179
191
|
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Non-interactive entrypoint for trusted local service managers such as
|
|
4
|
+
// COS Control. The existing glasses-server CLI remains the interactive setup
|
|
5
|
+
// path; this launcher assumes ~/.cos-glasses/.env is already configured.
|
|
6
|
+
|
|
7
|
+
const { resolve } = require('node:path')
|
|
8
|
+
const packageJson = require('../package.json')
|
|
9
|
+
|
|
10
|
+
const PKG_ROOT = resolve(__dirname, '..')
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
require('tsx/cjs')
|
|
14
|
+
} catch {
|
|
15
|
+
console.error('[cos-managed] Package dependencies are incomplete; reinstall @gotcos/glasses-server.')
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const workDir = process.env.COS_WORKDIR?.trim()
|
|
20
|
+
process.env.COS_MANAGED = '1'
|
|
21
|
+
process.env.COS_ENTRYPOINT = 'managed-server'
|
|
22
|
+
process.env.COS_SERVER_VERSION = packageJson.version
|
|
23
|
+
if (workDir) process.env.COS_LAUNCH_DIR = workDir
|
|
24
|
+
|
|
25
|
+
// The launchd-owned PID is the listener owner. Keeping the listener in this
|
|
26
|
+
// process removes the supervisor/child ambiguity that made lifecycle proof and
|
|
27
|
+
// crash receipts unreliable.
|
|
28
|
+
require(resolve(PKG_ROOT, 'server/index.ts'))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"contractVersion": 2,
|
|
4
|
+
"entrypoint": "bin/managed-server.cjs",
|
|
5
|
+
"listenerOwnership": "entrypoint-process",
|
|
6
|
+
"requiredEnvironment": [
|
|
7
|
+
"COS_API_TOKEN",
|
|
8
|
+
"COS_SERVER_GENERATION_ID",
|
|
9
|
+
"COS_MAINTENANCE_GATE_PATH"
|
|
10
|
+
],
|
|
11
|
+
"optionalEnvironment": [
|
|
12
|
+
"COS_WORKDIR",
|
|
13
|
+
"PORT",
|
|
14
|
+
"HTTPS_PORT",
|
|
15
|
+
"BIND_HOST",
|
|
16
|
+
"COS_DURABLE_QUERY_JOBS"
|
|
17
|
+
],
|
|
18
|
+
"maintenance": {
|
|
19
|
+
"scope": "cross_boot",
|
|
20
|
+
"adoptionRequired": true,
|
|
21
|
+
"networkRestartEndpoint": false
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.13.0",
|
|
4
4
|
"description": "COS Glasses — self-hosted AI heads-up-display server for Even G2 smart glasses, powered by your local Claude Code or Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"glasses-server": "bin/cli.cjs"
|
|
7
|
+
"glasses-server": "bin/cli.cjs",
|
|
8
|
+
"glasses-server-managed": "bin/managed-server.cjs"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "node bin/cli.cjs",
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
],
|
|
25
26
|
"files": [
|
|
26
27
|
"bin/cli.cjs",
|
|
28
|
+
"bin/managed-server.cjs",
|
|
29
|
+
"managed-runtime-contract.json",
|
|
27
30
|
"server",
|
|
28
31
|
"shared",
|
|
29
32
|
"!server/**/*.test.ts",
|
package/server/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { sessionsRouter } from './routes/sessions.js'
|
|
|
28
28
|
import { mediaRouter, mediaBodyParser } from './routes/media.js'
|
|
29
29
|
import { promptDraftsRouter } from './routes/prompt-drafts.js'
|
|
30
30
|
import { cliDebugRouter } from './routes/cli-debug.js'
|
|
31
|
+
import { maintenanceRouter } from './routes/maintenance.js'
|
|
31
32
|
import { prewarmContext } from './lib/context-builder.js'
|
|
32
33
|
import { preWarmCLI } from './lib/claude-bridge.js'
|
|
33
34
|
import { getCodexRunConfig } from './lib/codex-run-ledger.js'
|
|
@@ -58,6 +59,13 @@ import {
|
|
|
58
59
|
isTailscaleIpv4,
|
|
59
60
|
} from './lib/network-policy.js'
|
|
60
61
|
import { timingSafeTokenEqual } from './lib/token-auth.js'
|
|
62
|
+
import { isManagedRuntime } from './lib/managed-runtime.js'
|
|
63
|
+
import {
|
|
64
|
+
acquireMaintenanceWork,
|
|
65
|
+
MaintenanceLifecycleError,
|
|
66
|
+
maintenanceAdmissionsOpen,
|
|
67
|
+
maintenanceErrorPayload,
|
|
68
|
+
} from './lib/maintenance-lifecycle.js'
|
|
61
69
|
|
|
62
70
|
const app = express()
|
|
63
71
|
const PORT = parseInt(process.env.PORT ?? '3141', 10)
|
|
@@ -74,6 +82,9 @@ const BIND_HOST = process.env.BIND_HOST ?? '0.0.0.0'
|
|
|
74
82
|
|
|
75
83
|
// API token — auto-generate if not set so every session is authenticated.
|
|
76
84
|
const API_TOKEN_AUTO = !process.env.COS_API_TOKEN
|
|
85
|
+
if (isManagedRuntime() && API_TOKEN_AUTO) {
|
|
86
|
+
throw new Error('Managed COS startup requires a pre-provisioned COS_API_TOKEN.')
|
|
87
|
+
}
|
|
77
88
|
const API_TOKEN = process.env.COS_API_TOKEN ?? `_${randomBytes(32).toString('base64url')}`
|
|
78
89
|
process.env.COS_API_TOKEN = API_TOKEN // make available to routes that check it
|
|
79
90
|
// Persist an auto-generated token to ~/.cos-glasses/.env so it SURVIVES restarts.
|
|
@@ -135,6 +146,42 @@ app.use('/api', (req, res, next) => {
|
|
|
135
146
|
next()
|
|
136
147
|
})
|
|
137
148
|
|
|
149
|
+
// Fail-closed catch-all for mutation routes that do not own a more specific
|
|
150
|
+
// lifecycle lease below. This closes the admission/drain race for secondary
|
|
151
|
+
// state-changing APIs (media, sessions, settings, diagnostics) without
|
|
152
|
+
// double-owning provider and recording continuations whose routes retain work
|
|
153
|
+
// through their true terminal boundary.
|
|
154
|
+
app.use('/api', (req, res, next) => {
|
|
155
|
+
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next()
|
|
156
|
+
const lifecycleOwned = (req.path === '/query-jobs' && req.method === 'POST')
|
|
157
|
+
|| req.path === '/query'
|
|
158
|
+
|| req.path === '/transcribe'
|
|
159
|
+
|| req.path.startsWith('/transcribe-stream')
|
|
160
|
+
|| req.path === '/meeting/save'
|
|
161
|
+
|| req.path.startsWith('/prompt-drafts')
|
|
162
|
+
|| req.path.startsWith('/maintenance/drain')
|
|
163
|
+
if (lifecycleOwned) return next()
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const lease = acquireMaintenanceWork('api_mutation')
|
|
167
|
+
let released = false
|
|
168
|
+
const release = () => {
|
|
169
|
+
if (released) return
|
|
170
|
+
released = true
|
|
171
|
+
lease.release()
|
|
172
|
+
}
|
|
173
|
+
res.once('finish', release)
|
|
174
|
+
res.once('close', release)
|
|
175
|
+
next()
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error instanceof MaintenanceLifecycleError) {
|
|
178
|
+
if (error.retryAfterSeconds != null) res.setHeader('Retry-After', String(error.retryAfterSeconds))
|
|
179
|
+
return res.status(error.status).json(maintenanceErrorPayload(error))
|
|
180
|
+
}
|
|
181
|
+
return res.status(500).json({ error: 'maintenance_internal_error', retryable: false })
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
138
185
|
// Authenticate before parsing large upload bodies. The 16 MB allowance stays
|
|
139
186
|
// scoped to /api/media; every other route retains the 10 MB ceiling.
|
|
140
187
|
app.use('/api/media', mediaBodyParser)
|
|
@@ -167,6 +214,7 @@ app.use('/api', sessionsRouter)
|
|
|
167
214
|
app.use('/api', mediaRouter)
|
|
168
215
|
app.use('/api', promptDraftsRouter)
|
|
169
216
|
app.use('/api', cliDebugRouter)
|
|
217
|
+
app.use('/api', maintenanceRouter)
|
|
170
218
|
|
|
171
219
|
// OpenAI-compatible endpoint for the G2 Agent (ER "Add Agent")
|
|
172
220
|
// Mounted at root — routes are /v1/chat/completions and /v1/models
|
|
@@ -242,6 +290,7 @@ listeners.push({ server: httpServer, port: PORT, host: BIND_HOST, label: 'HTTP'
|
|
|
242
290
|
|
|
243
291
|
listenRequiredServers(listeners).then(() => {
|
|
244
292
|
const serverInstanceId = initializeServerInstanceId()
|
|
293
|
+
const startupAdmissionsOpen = maintenanceAdmissionsOpen()
|
|
245
294
|
if (listeners.some(listener => listener.label === 'HTTPS')) {
|
|
246
295
|
console.log(`[COS API] HTTPS server running on https://${BIND_HOST}:${HTTPS_PORT}`)
|
|
247
296
|
}
|
|
@@ -249,17 +298,21 @@ listenRequiredServers(listeners).then(() => {
|
|
|
249
298
|
console.log(`[COS API] Server instance: ${serverInstanceId}`)
|
|
250
299
|
console.log(`[COS API] Mode: ${COS_MODE ? 'COS pipeline' : 'standalone'}`)
|
|
251
300
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
301
|
+
if (startupAdmissionsOpen) {
|
|
302
|
+
void initQueryJobRuntime().then(health => {
|
|
303
|
+
if (process.env.COS_DURABLE_QUERY_JOBS === '1') {
|
|
304
|
+
console.log(`[COS API] Durable query jobs: ${health.store.state} · ${health.store.retainedIdentities} retained`)
|
|
305
|
+
} else {
|
|
306
|
+
console.log('[COS API] Durable query jobs: disabled (set COS_DURABLE_QUERY_JOBS=1 to enable)')
|
|
307
|
+
}
|
|
308
|
+
}).catch(error => {
|
|
309
|
+
// The store remains degraded and rejects admission. Legacy /api/query is
|
|
310
|
+
// still mounted, so disabling the feature flag is an immediate rollback.
|
|
311
|
+
console.error('[COS API] Durable query-job store unavailable:', error)
|
|
312
|
+
})
|
|
313
|
+
} else {
|
|
314
|
+
console.log('[COS API] Startup maintenance gate is closed — durable recovery waits for controller adoption')
|
|
315
|
+
}
|
|
263
316
|
|
|
264
317
|
// Print ADDRESSES THE PHONE CAN ACTUALLY REACH. The bind address (0.0.0.0) is
|
|
265
318
|
// not paste-able — enumerate real interfaces and label the Tailscale one.
|
|
@@ -281,8 +334,10 @@ listenRequiredServers(listeners).then(() => {
|
|
|
281
334
|
}
|
|
282
335
|
} catch { /* interface enumeration is best-effort */ }
|
|
283
336
|
|
|
284
|
-
//
|
|
285
|
-
|
|
337
|
+
// Interactive standalone startup may print a newly generated pairing token.
|
|
338
|
+
// Managed startup never generates or logs credentials; COS Control copies the
|
|
339
|
+
// pre-provisioned token through the local pasteboard flow instead.
|
|
340
|
+
if (API_TOKEN_AUTO && !isManagedRuntime()) {
|
|
286
341
|
console.log('')
|
|
287
342
|
console.log(`[COS API] API Token: ${API_TOKEN}`)
|
|
288
343
|
console.log('[COS API] ^ paste this into the COS Glasses app' + (API_TOKEN_PERSISTED ? ' — saved to ~/.cos-glasses/.env so it stays the same across restarts' : ' (set COS_API_TOKEN in .env for a fixed token)'))
|
|
@@ -306,32 +361,34 @@ listenRequiredServers(listeners).then(() => {
|
|
|
306
361
|
console.log(`[COS API] Codex workdir: ${codexConfig.cwd}`)
|
|
307
362
|
// Refresh immediately and then periodically. The catalog module retains the
|
|
308
363
|
// last-known-good snapshot if Codex is temporarily unavailable.
|
|
309
|
-
|
|
364
|
+
if (startupAdmissionsOpen) {
|
|
365
|
+
startCodexModelCatalogRefresh()
|
|
310
366
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
367
|
+
if (COS_MODE) {
|
|
368
|
+
initSessionCache()
|
|
369
|
+
// Pre-warm context cache so first query doesn't wait for the pipeline
|
|
370
|
+
prewarmContext()
|
|
371
|
+
}
|
|
372
|
+
// Start local whisper-server (model stays in RAM for ~50ms transcription)
|
|
373
|
+
startWhisperServer().catch(err => console.error('[startup] Whisper server error:', err))
|
|
374
|
+
// Initialize speaker embeddings (voiceprint-based diarization) — fails soft if model absent
|
|
375
|
+
const embeddingOk = initSpeakerEmbeddings()
|
|
376
|
+
console.log(`[startup] Speaker embeddings: ${embeddingOk ? 'active' : 'disabled (model not found)'}`)
|
|
377
|
+
// Initialize Silero VAD (silence trimming before Whisper) — fails soft if model absent
|
|
378
|
+
const vadOk = initSileroVAD()
|
|
379
|
+
console.log(`[startup] Silero VAD: ${vadOk ? 'active' : 'disabled (model not found)'}`)
|
|
324
380
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
381
|
+
// Pre-warm Claude only when installed so Codex-only startup stays quiet.
|
|
382
|
+
if (claudeAvailable) {
|
|
383
|
+
preWarmCLI().catch(err => console.error('[startup] CLI pre-warm error:', err))
|
|
384
|
+
}
|
|
329
385
|
|
|
330
|
-
|
|
331
|
-
|
|
386
|
+
// Auto-snapshot active sessions every 5 min (survives restarts)
|
|
387
|
+
startAutoSnapshot(5 * 60_000)
|
|
332
388
|
|
|
333
|
-
|
|
334
|
-
|
|
389
|
+
// Durable media GC (staged/reserved expiry + generated-image content TTL).
|
|
390
|
+
getMediaStore().startGC()
|
|
391
|
+
}
|
|
335
392
|
}).catch((error: NodeJS.ErrnoException) => {
|
|
336
393
|
console.error(`[COS API] Fatal listener startup: ${error.message}`)
|
|
337
394
|
process.exit(error.code === 'EADDRINUSE' ? 75 : 74)
|
package/server/lib/launch-dir.ts
CHANGED
|
@@ -14,16 +14,22 @@ import { join, resolve } from 'node:path'
|
|
|
14
14
|
|
|
15
15
|
let cached: string | null | undefined
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
const
|
|
21
|
-
if (!raw) { cached = null; return cached }
|
|
22
|
-
const dir = resolve(raw)
|
|
17
|
+
export function resolveCosBrainDir(raw: string | undefined): string | null {
|
|
18
|
+
const candidate = raw?.trim()
|
|
19
|
+
if (!candidate) return null
|
|
20
|
+
const dir = resolve(candidate)
|
|
23
21
|
const hasBrain =
|
|
24
22
|
existsSync(join(dir, '.cos', 'manifest.json')) ||
|
|
25
23
|
existsSync(join(dir, 'AGENTS.md')) ||
|
|
26
24
|
existsSync(join(dir, 'CLAUDE.md'))
|
|
27
|
-
|
|
25
|
+
return hasBrain ? dir : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The user's launch directory IF it contains a COS brain; otherwise null. */
|
|
29
|
+
export function cosBrainDir(): string | null {
|
|
30
|
+
if (cached !== undefined) return cached
|
|
31
|
+
// COS_WORKDIR is the provider-neutral managed setting. COS_LAUNCH_DIR stays
|
|
32
|
+
// as the interactive npx compatibility path.
|
|
33
|
+
cached = resolveCosBrainDir(process.env.COS_WORKDIR ?? process.env.COS_LAUNCH_DIR)
|
|
28
34
|
return cached
|
|
29
35
|
}
|