@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
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'))
|