@gotcos/glasses-server 6.2.0 → 6.2.1
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 +22 -0
- package/bin/cli.cjs +12 -0
- package/package.json +1 -1
- package/server/index.ts +36 -2
- package/server/lib/claude-bridge.ts +5 -4
- package/server/lib/codex-run-ledger.ts +5 -0
- package/server/lib/launch-dir.ts +29 -0
- package/server/routes/openai-compat.ts +2 -2
- package/server/routes/transcribe-stream.ts +6 -1
- package/shared/model-preference.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.2.1
|
|
4
|
+
|
|
5
|
+
Foolproofing release — driven by an adversarial onboarding QA pass.
|
|
6
|
+
|
|
7
|
+
- **The server now prints URLs the phone can actually use.** Boot output lists
|
|
8
|
+
your real addresses (`http://100.x.x.x:3141` labeled Tailscale, LAN IPs labeled
|
|
9
|
+
same-Wi-Fi) instead of only the un-pasteable bind address `0.0.0.0`.
|
|
10
|
+
- **Auto-generated API tokens survive restarts.** First boot saves the token to
|
|
11
|
+
`~/.cos-glasses/.env`, so re-running the server no longer silently rotates the
|
|
12
|
+
credential your app already saved (the "worked yesterday, 401 today" trap).
|
|
13
|
+
- **Starter-Kit COS inheritance is real now.** Run `npx @gotcos/glasses-server`
|
|
14
|
+
from your COS folder and glasses chat loads its brain: the launcher records
|
|
15
|
+
your launch directory, and when it contains `.cos/manifest.json`, `AGENTS.md`,
|
|
16
|
+
or `CLAUDE.md`, Claude/Codex spawn there (explicit `COS_SCRIPTS_DIR` still wins).
|
|
17
|
+
- **Transfer-integrity report actually surfaces.** 6.2.0 recorded lost chunks but
|
|
18
|
+
never returned them; the offline-session `finalize` response now includes
|
|
19
|
+
`transferIntegrity` (received/expected/missing/completeness) and a gap-aware
|
|
20
|
+
`transcript` with inline `[… audio gap …]` markers.
|
|
21
|
+
- **One default model everywhere: Sonnet.** The query router, the OpenAI-compat
|
|
22
|
+
surface, and CLI pre-warm all default to Sonnet (was a mix of Opus and Haiku).
|
|
23
|
+
Set `COS_G2_DEFAULT_MODEL` to override; per-query picks unchanged.
|
|
24
|
+
|
|
3
25
|
## 6.2.0
|
|
4
26
|
|
|
5
27
|
Reliability release — ports the hardening the full COS Glasses app shipped in June.
|
package/bin/cli.cjs
CHANGED
|
@@ -13,6 +13,12 @@ const { homedir } = require('os')
|
|
|
13
13
|
const PKG_ROOT = resolve(__dirname, '..')
|
|
14
14
|
const CONFIG_DIR = join(homedir(), '.cos-glasses')
|
|
15
15
|
|
|
16
|
+
// Record where the user ran `npx @gotcos/glasses-server` from. The server spawns
|
|
17
|
+
// with cwd = PKG_ROOT (the npx cache), so without this the user's Starter-Kit COS
|
|
18
|
+
// (AGENTS.md / CLAUDE.md / .cos/) in their launch folder would never be seen.
|
|
19
|
+
// Chat spawns of claude/codex use this dir when it contains a COS brain.
|
|
20
|
+
if (!process.env.COS_LAUNCH_DIR) process.env.COS_LAUNCH_DIR = process.cwd()
|
|
21
|
+
|
|
16
22
|
const green = (s) => `\x1b[32m${s}\x1b[0m`
|
|
17
23
|
const red = (s) => `\x1b[31m${s}\x1b[0m`
|
|
18
24
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
@@ -185,6 +191,12 @@ if (!process.env.BIND_HOST) {
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
// Step 7: start the bundled server
|
|
194
|
+
try {
|
|
195
|
+
const ld = process.env.COS_LAUNCH_DIR
|
|
196
|
+
if (ld && (existsSync(join(ld, '.cos', 'manifest.json')) || existsSync(join(ld, 'AGENTS.md')) || existsSync(join(ld, 'CLAUDE.md')))) {
|
|
197
|
+
console.log(green(' ✓') + ` COS detected in ${dim(ld)} — glasses chat will load its brain`)
|
|
198
|
+
}
|
|
199
|
+
} catch { /* detection is best-effort */ }
|
|
188
200
|
console.log('')
|
|
189
201
|
console.log(dim(' Starting server...'))
|
|
190
202
|
console.log('')
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -7,7 +7,9 @@ import cors from 'cors'
|
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import { createServer as createHttpsServer } from 'node:https'
|
|
10
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
10
|
+
import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs'
|
|
11
|
+
import { networkInterfaces, homedir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
11
13
|
import { execSync } from 'node:child_process'
|
|
12
14
|
import { randomBytes } from 'node:crypto'
|
|
13
15
|
import { healthRouter } from './routes/health.js'
|
|
@@ -43,6 +45,18 @@ const BIND_HOST = process.env.BIND_HOST ?? '0.0.0.0'
|
|
|
43
45
|
const API_TOKEN_AUTO = !process.env.COS_API_TOKEN
|
|
44
46
|
const API_TOKEN = process.env.COS_API_TOKEN ?? `_${randomBytes(32).toString('base64url')}`
|
|
45
47
|
process.env.COS_API_TOKEN = API_TOKEN // make available to routes that check it
|
|
48
|
+
// Persist an auto-generated token to ~/.cos-glasses/.env so it SURVIVES restarts.
|
|
49
|
+
// Without this, every re-run mints a new token and the app's saved token starts
|
|
50
|
+
// returning 401 — the symptom looks like a broken app, not a rotated credential.
|
|
51
|
+
let API_TOKEN_PERSISTED = false
|
|
52
|
+
if (API_TOKEN_AUTO) {
|
|
53
|
+
try {
|
|
54
|
+
const envDir = join(homedir(), '.cos-glasses')
|
|
55
|
+
mkdirSync(envDir, { recursive: true })
|
|
56
|
+
appendFileSync(join(envDir, '.env'), `\n# auto-generated by the server so the app token survives restarts\nCOS_API_TOKEN=${API_TOKEN}\n`)
|
|
57
|
+
API_TOKEN_PERSISTED = true
|
|
58
|
+
} catch { /* read-only home — token stays per-session */ }
|
|
59
|
+
}
|
|
46
60
|
|
|
47
61
|
// Server metrics — shared with /api/health for monitoring
|
|
48
62
|
export const serverMetrics = {
|
|
@@ -178,11 +192,31 @@ app.listen(PORT, BIND_HOST, () => {
|
|
|
178
192
|
console.log(`[COS API] HTTP server running on http://${BIND_HOST}:${PORT}`)
|
|
179
193
|
console.log(`[COS API] Mode: ${COS_MODE ? 'COS pipeline' : 'standalone'}`)
|
|
180
194
|
|
|
195
|
+
// Print ADDRESSES THE PHONE CAN ACTUALLY REACH. The bind address (0.0.0.0) is
|
|
196
|
+
// not paste-able — enumerate real interfaces and label the Tailscale one.
|
|
197
|
+
try {
|
|
198
|
+
const nets = networkInterfaces()
|
|
199
|
+
const addrs: Array<{ ip: string; label: string }> = []
|
|
200
|
+
for (const [name, infos] of Object.entries(nets)) {
|
|
201
|
+
for (const info of infos ?? []) {
|
|
202
|
+
if (info.family !== 'IPv4' || info.internal) continue
|
|
203
|
+
const isTailscale = info.address.startsWith('100.') || name.startsWith('utun') || name.startsWith('tailscale')
|
|
204
|
+
addrs.push({ ip: info.address, label: isTailscale ? 'Tailscale — works from anywhere' : `${name} — same Wi-Fi only` })
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (addrs.length > 0) {
|
|
208
|
+
addrs.sort((a, b) => Number(b.label.startsWith('Tailscale')) - Number(a.label.startsWith('Tailscale')))
|
|
209
|
+
console.log('')
|
|
210
|
+
console.log('[COS API] Server URL for the COS Glasses app:')
|
|
211
|
+
for (const a of addrs) console.log(`[COS API] http://${a.ip}:${PORT} (${a.label})`)
|
|
212
|
+
}
|
|
213
|
+
} catch { /* interface enumeration is best-effort */ }
|
|
214
|
+
|
|
181
215
|
// Print the full API token when auto-generated — the user pastes it into the app.
|
|
182
216
|
if (API_TOKEN_AUTO) {
|
|
183
217
|
console.log('')
|
|
184
218
|
console.log(`[COS API] API Token: ${API_TOKEN}`)
|
|
185
|
-
console.log('[COS API] ^ paste this into the COS Glasses app (set COS_API_TOKEN in .env for a fixed token)')
|
|
219
|
+
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)'))
|
|
186
220
|
console.log('')
|
|
187
221
|
}
|
|
188
222
|
|
|
@@ -12,11 +12,12 @@ import { join } from 'node:path'
|
|
|
12
12
|
import { appendFileSync } from 'node:fs'
|
|
13
13
|
import crypto from 'node:crypto'
|
|
14
14
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
15
|
+
import { cosBrainDir } from './launch-dir.js'
|
|
15
16
|
import { logTokenAudit } from './token-audit.js'
|
|
16
17
|
import { buildSystemPrompt, buildLightweightSystemPrompt, buildPrewarmSystemPrompt, getCachedContextInstant } from './context-builder.js'
|
|
17
18
|
import { getHistory, addExchange, formatHistoryForPrompt, getOrCreateSession, isNewSession, markSessionNotified, getSessionModel, getSessionRaw, replaceLastExchangeWithSummary, type ModelPreference, type PromptReference } from './conversation.js'
|
|
18
19
|
import { notifySessionStart, notifyExchange } from './telegram-notify.js'
|
|
19
|
-
import { isClaudeModel, type ClaudeModelPreference } from '../../shared/model-preference.js'
|
|
20
|
+
import { isClaudeModel, DEFAULT_MODEL, type ClaudeModelPreference } from '../../shared/model-preference.js'
|
|
20
21
|
import {
|
|
21
22
|
finishClaudeRun,
|
|
22
23
|
getClaudeEffortLevel,
|
|
@@ -182,7 +183,7 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
182
183
|
|
|
183
184
|
const proc = spawn('claude', [
|
|
184
185
|
'-p',
|
|
185
|
-
'--model',
|
|
186
|
+
'--model', DEFAULT_MODEL, // Must match default query model — --resume inherits session model
|
|
186
187
|
'--effort', getClaudeEffortLevel(),
|
|
187
188
|
'--output-format', 'stream-json',
|
|
188
189
|
'--verbose',
|
|
@@ -191,7 +192,7 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
191
192
|
], {
|
|
192
193
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
193
194
|
env,
|
|
194
|
-
cwd: COS_SCRIPTS_DIR ?? process.cwd(),
|
|
195
|
+
cwd: COS_SCRIPTS_DIR ?? cosBrainDir() ?? process.cwd(),
|
|
195
196
|
})
|
|
196
197
|
|
|
197
198
|
let buffer = ''
|
|
@@ -419,7 +420,7 @@ export async function callClaudeStreaming(
|
|
|
419
420
|
// Strip CLAUDECODE env var so claude -p doesn't think it's nested
|
|
420
421
|
const env = { ...process.env }
|
|
421
422
|
delete env.CLAUDECODE
|
|
422
|
-
const cliCwd = COS_SCRIPTS_DIR ?? process.cwd()
|
|
423
|
+
const cliCwd = COS_SCRIPTS_DIR ?? cosBrainDir() ?? process.cwd()
|
|
423
424
|
const inactivityMs = INACTIVITY_BY_MODEL[resolvedModel]
|
|
424
425
|
const defaultWallMax = WALL_MAX_BY_MODEL[resolvedModel]
|
|
425
426
|
const wallMax = isExtendedQuery(query) ? WALL_MAX_EXTENDED_MS : defaultWallMax
|
|
@@ -2,6 +2,7 @@ import crypto from 'node:crypto'
|
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
5
|
+
import { cosBrainDir } from './launch-dir.js'
|
|
5
6
|
import { CODEX_ENGINE_SESSION_TTL_MS, type CodexTrustMode } from './codex-engine-sessions.js'
|
|
6
7
|
import {
|
|
7
8
|
CODEX_HIGH_REASONING_EFFORT,
|
|
@@ -80,6 +81,10 @@ export function getCodexExecutionCwd(): string {
|
|
|
80
81
|
const configured = process.env.CODEX_GLASSES_WORKDIR?.trim()
|
|
81
82
|
if (configured) return resolve(configured)
|
|
82
83
|
if (COS_SCRIPTS_DIR) return resolve(COS_SCRIPTS_DIR, '..', '..')
|
|
84
|
+
// A Starter-Kit COS in the directory the user launched npx from — Codex
|
|
85
|
+
// loads its AGENTS.md brain natively when run there.
|
|
86
|
+
const brain = cosBrainDir()
|
|
87
|
+
if (brain) return brain
|
|
83
88
|
// Last resort when neither CODEX_GLASSES_WORKDIR nor COS_SCRIPTS_DIR is set:
|
|
84
89
|
// the server's own working dir (codex glasses is an optional, env-configured feature).
|
|
85
90
|
return process.cwd()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// COS brain auto-detection for the "glasses inherit your COS" promise.
|
|
2
|
+
//
|
|
3
|
+
// The npx launcher (bin/cli.cjs) records the directory the user ran `npx
|
|
4
|
+
// @gotcos/glasses-server` from as COS_LAUNCH_DIR before re-spawning the server
|
|
5
|
+
// with cwd = the package root. If that launch directory contains a COS brain
|
|
6
|
+
// (a Starter-Kit scaffold: .cos/manifest.json, AGENTS.md, or CLAUDE.md), chat
|
|
7
|
+
// spawns of claude/codex use IT as their working directory — so the CLIs load
|
|
8
|
+
// the user's brain exactly as they would in a terminal session in that folder.
|
|
9
|
+
//
|
|
10
|
+
// Precedence stays: COS_SCRIPTS_DIR (full COS pipeline) > detected brain dir >
|
|
11
|
+
// process.cwd(). Explicit config always wins.
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { join, resolve } from 'node:path'
|
|
14
|
+
|
|
15
|
+
let cached: string | null | undefined
|
|
16
|
+
|
|
17
|
+
/** The user's launch directory IF it contains a COS brain; otherwise null. */
|
|
18
|
+
export function cosBrainDir(): string | null {
|
|
19
|
+
if (cached !== undefined) return cached
|
|
20
|
+
const raw = process.env.COS_LAUNCH_DIR?.trim()
|
|
21
|
+
if (!raw) { cached = null; return cached }
|
|
22
|
+
const dir = resolve(raw)
|
|
23
|
+
const hasBrain =
|
|
24
|
+
existsSync(join(dir, '.cos', 'manifest.json')) ||
|
|
25
|
+
existsSync(join(dir, 'AGENTS.md')) ||
|
|
26
|
+
existsSync(join(dir, 'CLAUDE.md'))
|
|
27
|
+
cached = hasBrain ? dir : null
|
|
28
|
+
return cached
|
|
29
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Router } from 'express'
|
|
7
7
|
import { preWarmCLI, logLatency } from '../lib/claude-bridge.js'
|
|
8
8
|
import { callModelStreaming } from '../lib/model-router.js'
|
|
9
|
-
import { normalizeModelPreference, type ModelPreference } from '../../shared/model-preference.js'
|
|
9
|
+
import { normalizeModelPreference, DEFAULT_MODEL, type ModelPreference } from '../../shared/model-preference.js'
|
|
10
10
|
import { tryInstantResponse } from '../lib/response-cache.js'
|
|
11
11
|
import crypto from 'node:crypto'
|
|
12
12
|
|
|
@@ -79,7 +79,7 @@ function resolveModel(model?: string, _query?: string): ModelPreference {
|
|
|
79
79
|
if (model === 'cos-sonnet') return 'sonnet'
|
|
80
80
|
if (model === 'cos-haiku') return 'haiku'
|
|
81
81
|
if (model === 'cos-codex-high' || model === 'cos-codex') return 'codex-high'
|
|
82
|
-
return normalizeModelPreference(process.env.COS_G2_DEFAULT_MODEL) ??
|
|
82
|
+
return normalizeModelPreference(process.env.COS_G2_DEFAULT_MODEL) ?? DEFAULT_MODEL
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const MODEL_NAMES: Record<ModelPreference, string> = {
|
|
@@ -1170,11 +1170,16 @@ transcribeStreamRouter.post('/transcribe-stream/offline-sessions/:sessionId/fina
|
|
|
1170
1170
|
const chunks = getSessionChunks(sessionId)
|
|
1171
1171
|
if (!chunks || chunks.length === 0) throw makeHttpError(404, 'offline session has no chunks', 'session_not_found')
|
|
1172
1172
|
await drainSessionAudioWrites(sessionId)
|
|
1173
|
-
|
|
1173
|
+
// Gap-aware assembly: chunks lost in transit surface as inline
|
|
1174
|
+
// "[… audio gap …]" markers instead of being silently stitched over.
|
|
1175
|
+
const transcript = getSessionTranscript(sessionId, { withGaps: true }) ?? ''
|
|
1176
|
+
const transferIntegrity = analyzeTranscriptGaps(sessionId)
|
|
1174
1177
|
res.json({
|
|
1175
1178
|
sessionId,
|
|
1176
1179
|
chunks: chunks.length,
|
|
1177
1180
|
transcriptChars: transcript.length,
|
|
1181
|
+
transcript,
|
|
1182
|
+
transferIntegrity,
|
|
1178
1183
|
readyToSave: true,
|
|
1179
1184
|
})
|
|
1180
1185
|
} catch (err: unknown) {
|
|
@@ -2,7 +2,7 @@ export type ClaudeModelPreference = 'opus' | 'sonnet' | 'haiku'
|
|
|
2
2
|
export type CodexModelPreference = 'codex-high'
|
|
3
3
|
export type ModelPreference = ClaudeModelPreference | CodexModelPreference
|
|
4
4
|
|
|
5
|
-
export const DEFAULT_MODEL = '
|
|
5
|
+
export const DEFAULT_MODEL = 'sonnet' as const
|
|
6
6
|
export const CODEX_HIGH_MODEL: CodexModelPreference = 'codex-high'
|
|
7
7
|
|
|
8
8
|
// Optional codex model passed to `codex exec --model`. Empty (the default) means
|