@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.
Files changed (51) hide show
  1. package/.cos-profile.example.json +7 -0
  2. package/.env.example +44 -0
  3. package/CHANGELOG.md +25 -0
  4. package/LICENSE +21 -0
  5. package/README.md +78 -0
  6. package/bin/cli.cjs +203 -0
  7. package/package.json +53 -0
  8. package/server/env.ts +26 -0
  9. package/server/index.ts +211 -0
  10. package/server/lib/archive-budget.ts +65 -0
  11. package/server/lib/archive.ts +414 -0
  12. package/server/lib/atomic-fs.ts +50 -0
  13. package/server/lib/audio-enhance.ts +87 -0
  14. package/server/lib/claude-bridge.ts +682 -0
  15. package/server/lib/claude-circuit.ts +52 -0
  16. package/server/lib/claude-run-ledger.ts +279 -0
  17. package/server/lib/codex-bridge.ts +476 -0
  18. package/server/lib/codex-engine-sessions.ts +140 -0
  19. package/server/lib/codex-run-ledger.ts +298 -0
  20. package/server/lib/context-builder.ts +210 -0
  21. package/server/lib/conversation.ts +587 -0
  22. package/server/lib/data-dir.ts +20 -0
  23. package/server/lib/display-bus.ts +21 -0
  24. package/server/lib/display-format.ts +23 -0
  25. package/server/lib/fuzzy-correct.ts +286 -0
  26. package/server/lib/hallucination-filter.ts +469 -0
  27. package/server/lib/local-day.ts +13 -0
  28. package/server/lib/model-router.ts +38 -0
  29. package/server/lib/openai-key.ts +155 -0
  30. package/server/lib/openai-whisper-budget.ts +170 -0
  31. package/server/lib/profile.ts +94 -0
  32. package/server/lib/python-bridge.ts +84 -0
  33. package/server/lib/response-cache.ts +138 -0
  34. package/server/lib/session-cache-writer.ts +266 -0
  35. package/server/lib/session-log.ts +162 -0
  36. package/server/lib/speaker-embeddings.ts +578 -0
  37. package/server/lib/telegram-notify.ts +85 -0
  38. package/server/lib/token-audit.ts +50 -0
  39. package/server/lib/transcribe-audio.ts +187 -0
  40. package/server/lib/utils.ts +5 -0
  41. package/server/lib/vad-silero.ts +179 -0
  42. package/server/lib/whisper-local.ts +697 -0
  43. package/server/routes/diag.ts +115 -0
  44. package/server/routes/display.ts +65 -0
  45. package/server/routes/health.ts +128 -0
  46. package/server/routes/openai-compat.ts +446 -0
  47. package/server/routes/openai-key.ts +121 -0
  48. package/server/routes/query.ts +121 -0
  49. package/server/routes/transcribe-stream.ts +1090 -0
  50. package/server/routes/transcribe.ts +55 -0
  51. package/shared/model-preference.ts +81 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "owner_name": "Your Name",
3
+ "owner_speaker_label": "Me",
4
+ "vocabulary": ["NameOne", "NameTwo", "YourCompany", "ProductName"],
5
+ "whisper_corrections": "{\"Soundalike\": \"YourName\"}",
6
+ "negative_rules": []
7
+ }
package/.env.example ADDED
@@ -0,0 +1,44 @@
1
+ # ── COS Glasses Server — configuration ──────────────────────────────────
2
+ # Lives at ~/.cos-glasses/.env (created on first run). Everything here is
3
+ # OPTIONAL — the server boots and chats with zero config as long as the
4
+ # Claude Code CLI is installed and logged in.
5
+
6
+ # ── PHONE ACCESS ────────────────────────────────────────────────────────
7
+ # The glasses app (on your iPhone) reaches this server over your network or
8
+ # mesh. The launcher sets 0.0.0.0 for you; set 127.0.0.1 to restrict to the
9
+ # local machine only. (The IP allowlist always blocks public internet traffic.)
10
+ BIND_HOST=0.0.0.0
11
+
12
+ # HTTP port the server listens on. HTTPS auto-enables on 3143 if you add
13
+ # cert.pem + key.pem under server/certs/ (optional; iOS WebView prefers HTTPS).
14
+ # PORT=3141
15
+
16
+ # ── AUTH ────────────────────────────────────────────────────────────────
17
+ # A token is REQUIRED on every /api call. Leave UNSET and the server prints an
18
+ # auto-generated one at boot — paste it into the phone app. Set a fixed value
19
+ # here for a stable token across restarts.
20
+ # COS_API_TOKEN=pick-any-long-random-string
21
+
22
+ # ── THE LLM (chat) ──────────────────────────────────────────────────────
23
+ # Chat runs through your LOCAL agent CLI — NOT an API key:
24
+ # Opus / Sonnet / Haiku -> Claude Code CLI (https://claude.ai/download, then `claude login`)
25
+ # Codex High -> Codex CLI (https://developers.openai.com/codex/, then `codex login`)
26
+ # Install at least one. Pick the default model the glasses use:
27
+ # COS_G2_DEFAULT_MODEL=sonnet # opus | sonnet | haiku | codex-high
28
+ #
29
+ # Codex options (only used for the codex-high model):
30
+ # COS_CODEX_MODEL pins a specific codex model id; blank = use your codex CLI default.
31
+ # COS_CODEX_REASONING_EFFORT is low | medium | high (default high).
32
+ # COS_CODEX_MODEL=
33
+ # COS_CODEX_REASONING_EFFORT=high
34
+
35
+ # ── VOICE (optional) ────────────────────────────────────────────────────
36
+ # Local transcription is FREE via whisper.cpp (brew install whisper-cpp; the
37
+ # model auto-downloads on first run). If whisper.cpp is absent, voice falls
38
+ # back to the OpenAI API (~$0.006/min), which needs this key.
39
+ # OPENAI_API_KEY=sk-...
40
+
41
+ # ── FULL COS PIPELINE (optional — leave unset for standalone) ────────────
42
+ # Power users running the COS Starter Kit can point the glasses at their
43
+ # pipeline to inherit live tasks/calendar/people context. Omit for standalone.
44
+ # COS_SCRIPTS_DIR=/path/to/your/cos/operations/scripts
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 6.1.0
4
+
5
+ - **Codex backend.** Chat now routes to your local **Codex CLI** (`codex-high`) in
6
+ addition to Claude Code — pick either per query, or set `COS_G2_DEFAULT_MODEL`.
7
+ - The Codex model is **not** hardcoded — it uses your codex CLI's own default model
8
+ unless you pin one with `COS_CODEX_MODEL` (+ optional `COS_CODEX_REASONING_EFFORT`).
9
+ - Codex run/session state persists under `~/.cos-glasses/data`.
10
+
11
+ ## 6.0.0
12
+
13
+ The server now ships **inside** this package — `npx @gotcos/glasses-server` runs
14
+ it directly, with no second repository to clone.
15
+
16
+ - **Bundled server.** Previous versions cloned a separate app repo at runtime;
17
+ the standalone server is now part of the package tarball.
18
+ - **Standalone-first.** Glasses + your local Claude Code CLI. No API key is
19
+ pasted into the phone for chat.
20
+ - **Local voice.** Transcription runs on whisper.cpp (free); OpenAI API is an
21
+ optional fallback.
22
+ - **Phone reachability.** Defaults `BIND_HOST=0.0.0.0` so the glasses' phone app
23
+ can reach the server over your mesh/LAN. The IP allowlist blocks public traffic.
24
+ - **Persistent config** at `~/.cos-glasses/.env`.
25
+ - Requires Node.js 20.11+.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 COS Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # COS Glasses Server
2
+
3
+ Self-hosted AI heads-up display for **Even G2 smart glasses**. Runs on your Mac,
4
+ talks to your local **Claude Code** CLI, and pushes answers, voice
5
+ transcription, and notes to the lens. Your data never leaves your machine, and no
6
+ API key is pasted into the phone for chat.
7
+
8
+ ## Quick start
9
+
10
+ ```bash
11
+ npx @gotcos/glasses-server
12
+ ```
13
+
14
+ The launcher checks Node, finds your CLI, downloads the local voice model, writes
15
+ `~/.cos-glasses/.env`, and starts the server on `0.0.0.0:3141`. On boot it prints
16
+ an **API token** — paste that into the COS Glasses app.
17
+
18
+ ## Requirements
19
+
20
+ - **Node.js 20.11+** — https://nodejs.org
21
+ - **Claude Code CLI** (Opus/Sonnet/Haiku) — https://claude.ai/download, then `claude login`
22
+ _or_ **Codex CLI** (Codex High) — https://developers.openai.com/codex/, then `codex login`
23
+ - **Even G2 glasses** + the **COS Glasses** app from the Even Hub
24
+ - _Optional:_ `brew install whisper-cpp` for free local voice (otherwise OpenAI API)
25
+ - _Optional:_ **Tailscale** so your phone reaches your Mac from anywhere
26
+
27
+ > No `ANTHROPIC_API_KEY` is needed — chat runs through your installed CLI, billed
28
+ > to your existing Claude or Codex subscription. Pick either per query, or set a
29
+ > default with `COS_G2_DEFAULT_MODEL` (`opus`|`sonnet`|`haiku`|`codex-high`).
30
+ > Codex runs **sandboxed read-only** by default (`COS_CODEX_SANDBOX` to adjust).
31
+
32
+ ## Connect your phone (the one gotcha)
33
+
34
+ The glasses app runs on your iPhone and must reach this server on your Mac.
35
+
36
+ 1. The launcher binds `0.0.0.0` (all interfaces) for you.
37
+ 2. **Same WiFi (simplest):** find your Mac's LAN IP (System Settings > Wi-Fi > Details), and in the COS Glasses app enter `http://192.168.x.x:3141`.
38
+ 3. **From anywhere:** install **Tailscale** on the Mac + iPhone (same account), note the Mac's `100.x` address, and enter `http://100.x.x.x:3141`.
39
+ 4. Either way, paste the **API token** the server printed at boot.
40
+
41
+ To restrict the server to localhost only, set `BIND_HOST=127.0.0.1` in `~/.cos-glasses/.env`.
42
+ The built-in IP allowlist blocks public-internet traffic regardless.
43
+
44
+ ## What it does
45
+
46
+ - Ask anything, get a streamed answer on the lens (`/api/query`, `/v1/chat/completions`)
47
+ - Live voice capture + transcription during meetings
48
+ - Local whisper.cpp transcription (free) with OpenAI fallback (optional)
49
+ - Tasks / calendar / people context **if** you run the
50
+ [COS Starter Kit](https://www.gotcos.com) (`COS_SCRIPTS_DIR`); otherwise it is
51
+ glasses + AI only
52
+
53
+ ## Configuration
54
+
55
+ Config lives at `~/.cos-glasses/.env` (created on first run). Every key is
56
+ optional except an installed CLI. Highlights: `BIND_HOST`, `PORT`,
57
+ `COS_API_TOKEN` (auto if unset), `OPENAI_API_KEY` (cloud voice fallback),
58
+ `COS_SCRIPTS_DIR` (full pipeline). Your name + transcription vocabulary live in
59
+ `~/.cos-glasses/.cos-profile.json` (see `.cos-profile.example.json`).
60
+
61
+ ## Run from source
62
+
63
+ ```bash
64
+ git clone https://github.com/ukaoma/cos-glasses-server.git
65
+ cd cos-glasses-server
66
+ npm install
67
+ BIND_HOST=0.0.0.0 npm run start:server
68
+ ```
69
+
70
+ ## Troubleshooting
71
+
72
+ - *Phone can't connect* — check `BIND_HOST=0.0.0.0`, the same Tailscale account on both devices, and the correct `100.x` IP + token.
73
+ - *AI queries fail* — run `claude --version` / `codex --version`, then `claude login` / `codex login`.
74
+ - *Voice getting billed?* — install `whisper-cpp` for free local transcription.
75
+
76
+ ## License
77
+
78
+ MIT. Learn more at [gotcos.com](https://www.gotcos.com).
package/bin/cli.cjs ADDED
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+
3
+ // COS Glasses Server launcher.
4
+ // Runs the bundled server for Even G2 smart glasses. The server ships INSIDE this
5
+ // package — there is no clone. Config persists at ~/.cos-glasses/.
6
+
7
+ const { execSync, spawn } = require('child_process')
8
+ const { existsSync, mkdirSync, statSync, readFileSync, copyFileSync, unlinkSync, renameSync } = require('fs')
9
+ const { join, resolve } = require('path')
10
+ const { homedir } = require('os')
11
+
12
+ // bin/cli.cjs -> package root is one level up. The server lives at <root>/server.
13
+ const PKG_ROOT = resolve(__dirname, '..')
14
+ const CONFIG_DIR = join(homedir(), '.cos-glasses')
15
+
16
+ const green = (s) => `\x1b[32m${s}\x1b[0m`
17
+ const red = (s) => `\x1b[31m${s}\x1b[0m`
18
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`
19
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`
20
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`
21
+
22
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
23
+ console.log('')
24
+ console.log(bold(' COS Glasses Server'))
25
+ console.log('')
26
+ console.log(' Usage:')
27
+ console.log(' npx @gotcos/glasses-server')
28
+ console.log('')
29
+ console.log(' Requirements:')
30
+ console.log(' - Node.js 20.11+')
31
+ console.log(' - Claude Code CLI (Opus / Sonnet / Haiku) or Codex CLI (Codex High)')
32
+ console.log(' - Even G2 smart glasses + the COS Glasses app (Even Hub)')
33
+ console.log('')
34
+ console.log(' No API key is needed for chat — it runs through your installed CLI.')
35
+ console.log(' Config persists at ~/.cos-glasses/.env')
36
+ console.log('')
37
+ console.log(' Setup guide: https://www.gotcos.com')
38
+ console.log('')
39
+ process.exit(0)
40
+ }
41
+
42
+ console.log('')
43
+ console.log(bold(' COS Glasses Server'))
44
+ console.log(dim(' AI heads-up display for Even G2 smart glasses'))
45
+ console.log('')
46
+
47
+ // Step 1: Node version. The server uses import.meta.dirname (Node 20.11+ / 21.2+).
48
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map((n) => parseInt(n, 10))
49
+ if (nodeMajor < 20 || (nodeMajor === 20 && nodeMinor < 11)) {
50
+ console.log(red(' ✗ Node.js 20.11+ required') + ` (you have ${process.versions.node})`)
51
+ console.log(' Update: https://nodejs.org')
52
+ process.exit(1)
53
+ }
54
+ console.log(green(' ✓') + ` Node.js ${process.versions.node}`)
55
+
56
+ // Step 2: agent CLI detection — at least one of Claude Code / Codex is required
57
+ function getCliVersion(command) {
58
+ try {
59
+ return execSync(`${command} --version 2>&1`, { shell: '/bin/sh', stdio: 'pipe', timeout: 5000 }).toString().trim()
60
+ } catch {
61
+ return null
62
+ }
63
+ }
64
+ function normalizeCodexVersion(raw) {
65
+ if (!raw) return 'available'
66
+ const line = raw.split('\n').map((s) => s.trim()).find((s) => /^codex(?:-cli)?\s+/i.test(s)) || raw.split('\n')[0].trim()
67
+ return line.replace(/^codex(?:-cli)?\s*/i, '') || line
68
+ }
69
+ const claudeVersion = getCliVersion('claude')
70
+ const codexVersion = getCliVersion('codex')
71
+ if (claudeVersion) {
72
+ console.log(green(' ✓') + ` Claude Code ${claudeVersion} ` + dim('(Opus / Sonnet / Haiku)'))
73
+ } else {
74
+ console.log(yellow(' ⚠') + ' Claude Code CLI not found ' + dim('— Opus/Sonnet/Haiku unavailable'))
75
+ console.log(' Install: ' + bold('https://claude.ai/download'))
76
+ }
77
+ if (codexVersion) {
78
+ console.log(green(' ✓') + ` Codex CLI ${normalizeCodexVersion(codexVersion)} ` + dim('(Codex High)'))
79
+ } else {
80
+ console.log(yellow(' ⚠') + ' Codex CLI not found ' + dim('— Codex High unavailable'))
81
+ }
82
+ if (!claudeVersion && !codexVersion) {
83
+ console.log('')
84
+ console.log(red(' ✗ No supported agent CLI found'))
85
+ console.log(' Install Claude Code: ' + bold('https://claude.ai/download'))
86
+ console.log(' or Codex CLI: ' + bold('https://developers.openai.com/codex/') + ' then ' + bold('codex login'))
87
+ console.log('')
88
+ process.exit(1)
89
+ }
90
+
91
+ // Step 3: dependencies — only when run from a git clone without node_modules.
92
+ // `npx` already installed the package's deps alongside it.
93
+ if (!existsSync(join(PKG_ROOT, 'node_modules'))) {
94
+ console.log('')
95
+ console.log(` ${yellow('⟳')} Installing dependencies...`)
96
+ try {
97
+ execSync('npm install', { cwd: PKG_ROOT, stdio: 'pipe', timeout: 300000 })
98
+ console.log(green(' ✓') + ' Dependencies installed')
99
+ } catch (err) {
100
+ console.log(red(' ✗ npm install failed'))
101
+ console.log(` ${err.stderr?.toString().slice(0, 200) || err.message}`)
102
+ process.exit(1)
103
+ }
104
+ }
105
+
106
+ // Step 4: persistent config at ~/.cos-glasses/ (survives npx cache churn)
107
+ mkdirSync(CONFIG_DIR, { recursive: true })
108
+ const ENV_FILE = join(CONFIG_DIR, '.env')
109
+ const ENV_EXAMPLE = join(PKG_ROOT, '.env.example')
110
+ if (!existsSync(ENV_FILE) && existsSync(ENV_EXAMPLE)) {
111
+ copyFileSync(ENV_EXAMPLE, ENV_FILE)
112
+ console.log(green(' ✓') + ` Created config at ${dim(ENV_FILE)}`)
113
+ }
114
+ if (existsSync(ENV_FILE)) {
115
+ try {
116
+ for (const line of readFileSync(ENV_FILE, 'utf-8').split('\n')) {
117
+ const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)=(.*)$/)
118
+ if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim()
119
+ }
120
+ } catch { /* config is optional */ }
121
+ }
122
+ // Persistent profile (identity + transcription vocabulary)
123
+ const PROFILE_FILE = join(CONFIG_DIR, '.cos-profile.json')
124
+ const PROFILE_EXAMPLE = join(PKG_ROOT, '.cos-profile.example.json')
125
+ if (!existsSync(PROFILE_FILE) && existsSync(PROFILE_EXAMPLE)) {
126
+ copyFileSync(PROFILE_EXAMPLE, PROFILE_FILE)
127
+ }
128
+ if (!process.env.COS_PROFILE_PATH) process.env.COS_PROFILE_PATH = PROFILE_FILE
129
+
130
+ // Step 5: local Whisper detection + model download (free voice; OpenAI fallback otherwise)
131
+ const WHISPER_KNOWN_PATHS = ['/opt/homebrew/bin/whisper-cli', '/usr/local/bin/whisper-cli']
132
+ const WHISPER_MODEL_DIR = join(homedir(), '.local/share/whisper-models')
133
+ const WHISPER_MODEL_PATH = join(WHISPER_MODEL_DIR, 'ggml-large-v3-turbo.bin')
134
+ const WHISPER_MODEL_PARTIAL = WHISPER_MODEL_PATH + '.partial'
135
+ const WHISPER_MODEL_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin'
136
+ const WHISPER_MODEL_MIN_BYTES = 800_000_000
137
+ function findWhisperCli() {
138
+ for (const p of WHISPER_KNOWN_PATHS) { if (existsSync(p)) return p }
139
+ try {
140
+ const found = execSync('command -v whisper-cli 2>/dev/null', { shell: '/bin/sh', stdio: 'pipe', timeout: 2000 }).toString().trim()
141
+ return found || null
142
+ } catch {
143
+ return null
144
+ }
145
+ }
146
+ function isValidWhisperModel(p) {
147
+ if (!existsSync(p)) return false
148
+ try { return statSync(p).size >= WHISPER_MODEL_MIN_BYTES } catch { return false }
149
+ }
150
+ const whisperCliPath = findWhisperCli()
151
+ const hasValidModel = isValidWhisperModel(WHISPER_MODEL_PATH)
152
+ if (whisperCliPath && hasValidModel) {
153
+ console.log(green(' ✓') + ' whisper.cpp + model ready ' + dim('— voice = local (FREE)'))
154
+ } else if (whisperCliPath && !hasValidModel) {
155
+ if (existsSync(WHISPER_MODEL_PATH)) { try { unlinkSync(WHISPER_MODEL_PATH) } catch {} }
156
+ if (existsSync(WHISPER_MODEL_PARTIAL)) { try { unlinkSync(WHISPER_MODEL_PARTIAL) } catch {} }
157
+ console.log(yellow(' ⚠') + ' whisper.cpp installed but model missing')
158
+ console.log(' ' + dim('Downloading ggml-large-v3-turbo (~1.5 GB). Ctrl-C to skip (uses OpenAI API instead).'))
159
+ console.log(' ' + dim('Skip permanently: SKIP_WHISPER_DOWNLOAD=1 npx @gotcos/glasses-server'))
160
+ if (process.env.SKIP_WHISPER_DOWNLOAD === '1') {
161
+ console.log(yellow(' ⚠') + ' SKIP_WHISPER_DOWNLOAD=1 — voice will use OpenAI API')
162
+ } else {
163
+ try {
164
+ mkdirSync(WHISPER_MODEL_DIR, { recursive: true })
165
+ execSync(`curl -fL --progress-bar "${WHISPER_MODEL_URL}" -o "${WHISPER_MODEL_PARTIAL}"`, { stdio: 'inherit', timeout: 900000 })
166
+ const stats = statSync(WHISPER_MODEL_PARTIAL)
167
+ if (stats.size < WHISPER_MODEL_MIN_BYTES) throw new Error(`Downloaded file too small: ${stats.size} bytes`)
168
+ renameSync(WHISPER_MODEL_PARTIAL, WHISPER_MODEL_PATH)
169
+ console.log(green(' ✓') + ' Model downloaded ' + dim('— voice = local (FREE)'))
170
+ } catch (err) {
171
+ try { unlinkSync(WHISPER_MODEL_PARTIAL) } catch {}
172
+ console.log(red(' ✗') + ' Model download failed ' + dim('— voice will use OpenAI API'))
173
+ console.log(' ' + dim('Error: ' + (err.message || err).toString().slice(0, 120)))
174
+ }
175
+ }
176
+ } else {
177
+ console.log(yellow(' ⚠') + ' whisper.cpp not installed ' + dim('— voice will use OpenAI API ($0.006/min)'))
178
+ console.log(' Free local voice: ' + bold('brew install whisper-cpp') + dim(' (no Homebrew? https://brew.sh)'))
179
+ }
180
+
181
+ // Step 6: phone reachability — the glasses' phone app must reach this server.
182
+ if (!process.env.BIND_HOST) {
183
+ process.env.BIND_HOST = '0.0.0.0'
184
+ console.log(yellow(' ⚠') + ' BIND_HOST not set — defaulting to 0.0.0.0 so your phone can reach the server')
185
+ }
186
+
187
+ // Step 7: start the bundled server
188
+ console.log('')
189
+ console.log(dim(' Starting server...'))
190
+ console.log('')
191
+ const serverProc = spawn(
192
+ process.execPath,
193
+ ['--import', 'tsx/esm', 'server/index.ts'],
194
+ { cwd: PKG_ROOT, stdio: 'inherit', env: { ...process.env } }
195
+ )
196
+ serverProc.on('error', (err) => {
197
+ console.error(red(` Server failed to start: ${err.message}`))
198
+ if (err.message.includes('tsx')) console.error(' Try: cd ' + PKG_ROOT + ' && npm install')
199
+ process.exit(1)
200
+ })
201
+ serverProc.on('exit', (code) => process.exit(code ?? 0))
202
+ process.on('SIGINT', () => serverProc.kill('SIGINT'))
203
+ process.on('SIGTERM', () => serverProc.kill('SIGTERM'))
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@gotcos/glasses-server",
3
+ "version": "6.1.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "glasses-server": "bin/cli.cjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/cli.cjs",
11
+ "start:server": "node --import tsx/esm server/index.ts",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "keywords": ["even-g2", "smart-glasses", "claude-code", "codex", "ai", "cos", "heads-up-display", "whisper"],
15
+ "files": [
16
+ "bin/cli.cjs",
17
+ "server/**/*.ts",
18
+ "server/**/*.js",
19
+ "shared/**/*.ts",
20
+ ".env.example",
21
+ ".cos-profile.example.json",
22
+ "README.md",
23
+ "LICENSE",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "author": "COS Contributors",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/ukaoma/cos-glasses-server.git"
31
+ },
32
+ "homepage": "https://www.gotcos.com",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "node": ">=20.11"
38
+ },
39
+ "dependencies": {
40
+ "cors": "^2.8.6",
41
+ "express": "^5.2.1",
42
+ "tsx": "^4.21.0"
43
+ },
44
+ "optionalDependencies": {
45
+ "sherpa-onnx-node": "^1.12.29"
46
+ },
47
+ "devDependencies": {
48
+ "@types/cors": "^2.8.19",
49
+ "@types/express": "^5.0.6",
50
+ "@types/node": "^22.10.0",
51
+ "typescript": "^5.9.3"
52
+ }
53
+ }
package/server/env.ts ADDED
@@ -0,0 +1,26 @@
1
+ // .env loader — MUST be imported first in index.ts so process.env is populated
2
+ // before python-bridge.ts reads COS_SCRIPTS_DIR.
3
+ //
4
+ // Loads the persistent launcher config (~/.cos-glasses/.env) first, then a
5
+ // repo-local .env for from-source/dev runs. Existing process.env values always
6
+ // win (the launcher pre-injects them), so this is a safe last-resort loader.
7
+
8
+ import { readFileSync } from 'node:fs'
9
+ import { resolve, join } from 'node:path'
10
+ import { homedir } from 'node:os'
11
+
12
+ function loadEnvFile(path: string): void {
13
+ try {
14
+ const content = readFileSync(path, 'utf-8')
15
+ for (const line of content.split('\n')) {
16
+ // Allow digits in keys (e.g. COS_G2_DEFAULT_MODEL) and empty values.
17
+ const match = line.match(/^\s*([A-Z_][A-Z0-9_]*)=(.*)$/)
18
+ if (match && !process.env[match[1]]) {
19
+ process.env[match[1]] = match[2].trim()
20
+ }
21
+ }
22
+ } catch { /* .env is optional — env vars can be set externally */ }
23
+ }
24
+
25
+ loadEnvFile(join(homedir(), '.cos-glasses', '.env'))
26
+ loadEnvFile(resolve(import.meta.dirname, '../.env'))