@gotcos/glasses-server 6.38.0 → 6.39.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.
@@ -0,0 +1,222 @@
1
+ import { findBannedPermissionArg } from './banned-permission-args.js'
2
+
3
+ export const CODEX_EXTRA_ARGS_MAX_TOKENS = 32
4
+ export const CODEX_EXTRA_ARGS_MAX_CHARS = 2_000
5
+
6
+ const BOOLEAN_FLAGS = new Set(['--oss'])
7
+ const VALUE_FLAGS = new Set(['-c', '--config', '--model', '-m', '--local-provider'])
8
+ const ALLOWED_FLAGS = new Set(['--oss', '--local-provider', '--model', '-m', '-c', '--config'])
9
+ const ALLOWED_CONFIG_KEYS = new Set([
10
+ 'model',
11
+ 'model_provider',
12
+ 'oss_provider',
13
+ 'model_reasoning_effort',
14
+ 'service_tier',
15
+ ])
16
+ const LOCAL_PROVIDER_VALUES = new Set(['ollama', 'lmstudio'])
17
+
18
+ export class CodexExtraArgsError extends Error {
19
+ flag: string
20
+ constructor(flag: string) {
21
+ super(`Invalid COS_CODEX_EXTRA_ARGS (${flag}). Edit ~/.cos-glasses/.env and restart the glasses server.`)
22
+ this.name = 'CodexExtraArgsError'
23
+ this.flag = flag
24
+ }
25
+ }
26
+
27
+ function extraArgsUserMessage(flag: string): string {
28
+ return `Invalid COS_CODEX_EXTRA_ARGS (${flag}). Edit ~/.cos-glasses/.env and restart the glasses server.`
29
+ }
30
+
31
+ export function extraArgsErrorMessage(flag: string): string {
32
+ return extraArgsUserMessage(flag)
33
+ }
34
+
35
+ function unwrapWholeValue(raw: string): string {
36
+ if (raw.length >= 2) {
37
+ const first = raw[0]
38
+ const last = raw[raw.length - 1]
39
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
40
+ return raw.slice(1, -1).trim()
41
+ }
42
+ }
43
+ return raw
44
+ }
45
+
46
+ function splitUnquotedWhitespace(raw: string): string[] {
47
+ const tokens: string[] = []
48
+ let current = ''
49
+ let quote: '"' | "'" | null = null
50
+ for (const ch of raw) {
51
+ if (quote) {
52
+ current += ch
53
+ if (ch === quote) quote = null
54
+ continue
55
+ }
56
+ if (ch === '"' || ch === "'") {
57
+ quote = ch
58
+ current += ch
59
+ continue
60
+ }
61
+ if (/\s/.test(ch)) {
62
+ if (current) {
63
+ tokens.push(current)
64
+ current = ''
65
+ }
66
+ continue
67
+ }
68
+ current += ch
69
+ }
70
+ if (quote) throw new CodexExtraArgsError('quotes')
71
+ if (current) tokens.push(current)
72
+ return tokens
73
+ }
74
+
75
+ export function parseCodexExtraArgs(raw: string | undefined): string[] {
76
+ const trimmed = (raw ?? '').trim()
77
+ if (!trimmed) return []
78
+ if (trimmed.length > CODEX_EXTRA_ARGS_MAX_CHARS) throw new CodexExtraArgsError('length')
79
+ const unwrapped = unwrapWholeValue(trimmed)
80
+ if (!unwrapped) return []
81
+ const tokens = splitUnquotedWhitespace(unwrapped)
82
+ if (tokens.length > CODEX_EXTRA_ARGS_MAX_TOKENS) throw new CodexExtraArgsError('length')
83
+ return tokens
84
+ }
85
+
86
+ function stripWrappingQuotes(value: string): string {
87
+ if (value.length >= 2) {
88
+ const first = value[0]
89
+ const last = value[value.length - 1]
90
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
91
+ return value.slice(1, -1)
92
+ }
93
+ }
94
+ return value
95
+ }
96
+
97
+ function fusedValueFlag(token: string): string | null {
98
+ for (const flag of VALUE_FLAGS) {
99
+ if (token.startsWith(`${flag}=`)) return flag
100
+ }
101
+ return null
102
+ }
103
+
104
+ type Walked =
105
+ | { kind: 'boolean'; flag: string }
106
+ | { kind: 'value'; flag: string; value: string }
107
+
108
+ function walkCodexExtraArgs(args: string[]): Walked[] {
109
+ const walked: Walked[] = []
110
+ let i = 0
111
+ while (i < args.length) {
112
+ const token = args[i]
113
+ if (BOOLEAN_FLAGS.has(token)) {
114
+ walked.push({ kind: 'boolean', flag: token })
115
+ i += 1
116
+ continue
117
+ }
118
+ const fused = fusedValueFlag(token)
119
+ if (fused) {
120
+ const value = token.slice(fused.length + 1)
121
+ if (!value) throw new CodexExtraArgsError(fused)
122
+ if (value.startsWith('-')) throw new CodexExtraArgsError(fused)
123
+ walked.push({ kind: 'value', flag: fused, value })
124
+ i += 1
125
+ continue
126
+ }
127
+ if (VALUE_FLAGS.has(token)) {
128
+ const value = args[i + 1]
129
+ if (value === undefined) throw new CodexExtraArgsError(token)
130
+ if (value.startsWith('-')) throw new CodexExtraArgsError(token)
131
+ walked.push({ kind: 'value', flag: token, value })
132
+ i += 2
133
+ continue
134
+ }
135
+ throw new CodexExtraArgsError(token)
136
+ }
137
+ return walked
138
+ }
139
+
140
+ function configPayloads(walked: Walked[]): { key: string; value: string }[] {
141
+ const payloads: { key: string; value: string }[] = []
142
+ for (const step of walked) {
143
+ if (step.kind !== 'value' || (step.flag !== '-c' && step.flag !== '--config')) continue
144
+ const eq = step.value.indexOf('=')
145
+ if (eq <= 0) throw new CodexExtraArgsError(`-c ${stripWrappingQuotes(step.value)}`)
146
+ const key = stripWrappingQuotes(step.value.slice(0, eq))
147
+ const value = step.value.slice(eq + 1)
148
+ payloads.push({ key, value })
149
+ }
150
+ return payloads
151
+ }
152
+
153
+ export function extraIncludesFlag(args: string[], flags: string[]): boolean {
154
+ for (const token of args) {
155
+ for (const flag of flags) {
156
+ if (token === flag || token.startsWith(`${flag}=`)) return true
157
+ }
158
+ }
159
+ return false
160
+ }
161
+
162
+ export function extraIncludesConfigKey(args: string[], key: string): boolean {
163
+ const walked = walkCodexExtraArgs(args)
164
+ return configPayloads(walked).some(payload => payload.key === key)
165
+ }
166
+
167
+ export function assertSafeCodexExtraArgs(args: string[]): void {
168
+ if (args.length === 0) return
169
+ const walked = walkCodexExtraArgs(args)
170
+ for (const step of walked) {
171
+ if (!ALLOWED_FLAGS.has(step.flag)) throw new CodexExtraArgsError(step.flag)
172
+ }
173
+ const hasOss = walked.some(step => step.kind === 'boolean' && step.flag === '--oss')
174
+ const hasLocalProvider = extraIncludesFlag(args, ['--local-provider'])
175
+ const payloads = configPayloads(walked)
176
+ const hasOssProvider = payloads.some(payload => payload.key === 'oss_provider')
177
+ const hasModelProvider = payloads.some(payload => payload.key === 'model_provider')
178
+
179
+ if (hasOss && hasModelProvider) throw new CodexExtraArgsError('-c model_provider')
180
+ if (hasOss && !hasLocalProvider && !hasOssProvider) throw new CodexExtraArgsError('--oss')
181
+ if ((hasLocalProvider || hasOssProvider) && !hasOss) {
182
+ throw new CodexExtraArgsError(hasLocalProvider ? '--local-provider' : '-c oss_provider')
183
+ }
184
+
185
+ for (const step of walked) {
186
+ if (step.kind !== 'value') continue
187
+ if (step.flag === '--local-provider') {
188
+ if (!LOCAL_PROVIDER_VALUES.has(stripWrappingQuotes(step.value))) {
189
+ throw new CodexExtraArgsError('--local-provider')
190
+ }
191
+ }
192
+ }
193
+ for (const payload of payloads) {
194
+ if (!ALLOWED_CONFIG_KEYS.has(payload.key)) throw new CodexExtraArgsError(`-c ${payload.key}`)
195
+ if (payload.key === 'oss_provider' && !LOCAL_PROVIDER_VALUES.has(stripWrappingQuotes(payload.value))) {
196
+ throw new CodexExtraArgsError('-c oss_provider')
197
+ }
198
+ }
199
+
200
+ const banned = findBannedPermissionArg(args)
201
+ if (banned !== null) throw new CodexExtraArgsError(banned)
202
+ }
203
+
204
+ export function skipsCosModel(extra: string[]): boolean {
205
+ return extraIncludesFlag(extra, ['--model', '-m']) || extraIncludesConfigKey(extra, 'model')
206
+ }
207
+
208
+ export function skipsCosServiceTier(extra: string[]): boolean {
209
+ return extraIncludesFlag(extra, ['--oss', '--local-provider'])
210
+ || extraIncludesConfigKey(extra, 'model_provider')
211
+ || extraIncludesConfigKey(extra, 'service_tier')
212
+ }
213
+
214
+ export function skipsCosReasoningEffort(extra: string[]): boolean {
215
+ return extraIncludesConfigKey(extra, 'model_reasoning_effort')
216
+ || extraIncludesFlag(extra, ['--oss', '--local-provider'])
217
+ || extraIncludesConfigKey(extra, 'model_provider')
218
+ }
219
+
220
+ export function codexEngineFingerprint(extra: string[]): string {
221
+ return extra.join('\0')
222
+ }
@@ -0,0 +1,116 @@
1
+ // Even Hub 0.0.14 wearer-vs-other histogram, carried on a meeting chunk.
2
+ // Identity is a suggestion. This module parses and logs. It does not name
3
+ // people and does not change identifyChunkSpeaker.
4
+
5
+ export type EvenSpeakerRole = 'self' | 'other' | 'unknown'
6
+ export type EvenSpeakerRoleMajority = EvenSpeakerRole | 'tie'
7
+
8
+ export interface EvenSpeakerRoleHistogram {
9
+ schema: 1
10
+ frames: number
11
+ self: number
12
+ other: number
13
+ unknown: number
14
+ majority: EvenSpeakerRoleMajority
15
+ directionPresent: number
16
+ directionLast: number | null
17
+ }
18
+
19
+ export type EvenSpeakerRoleMode = 'off' | 'log' | 'apply'
20
+
21
+ export function evenSpeakerRoleMode(): EvenSpeakerRoleMode {
22
+ const raw = (process.env.COS_EVEN_SPEAKER_ROLE ?? 'log').trim().toLowerCase()
23
+ if (raw === 'off' || raw === '0' || raw === 'false') return 'off'
24
+ if (raw === 'apply') return 'apply'
25
+ return 'log'
26
+ }
27
+
28
+ let applyNotImplementedWarned = false
29
+
30
+ /** Gate A is not in this slice. apply must not silently change labels. */
31
+ export function warnEvenSpeakerRoleApplyNotImplemented(): void {
32
+ if (evenSpeakerRoleMode() !== 'apply' || applyNotImplementedWarned) return
33
+ applyNotImplementedWarned = true
34
+ console.warn('[even-role] COS_EVEN_SPEAKER_ROLE=apply is not implemented; logging only')
35
+ }
36
+
37
+ function majorityOf(self: number, other: number, unknown: number, frames: number): EvenSpeakerRoleMajority {
38
+ if (frames <= 0) return 'unknown'
39
+ if (self > other && self > unknown) return 'self'
40
+ if (other > self && other > unknown) return 'other'
41
+ if (unknown > self && unknown > other) return 'unknown'
42
+ return 'tie'
43
+ }
44
+
45
+ function asNonNegInt(raw: unknown): number | null {
46
+ const n = typeof raw === 'number' ? raw : typeof raw === 'string' && raw !== '' ? Number(raw) : NaN
47
+ if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) return null
48
+ return n
49
+ }
50
+
51
+ /** Compact query `eh=self,other,unknown,frames,directionPresent,directionLast`. */
52
+ export function parseEvenHubSpeakerRoleQuery(raw: unknown): EvenSpeakerRoleHistogram | undefined {
53
+ if (typeof raw !== 'string' || raw.length === 0) return undefined
54
+ const parts = raw.split(',')
55
+ if (parts.length < 4 || parts.length > 6) return undefined
56
+ const self = asNonNegInt(parts[0])
57
+ const other = asNonNegInt(parts[1])
58
+ const unknown = asNonNegInt(parts[2])
59
+ const frames = asNonNegInt(parts[3])
60
+ if (self == null || other == null || unknown == null || frames == null) return undefined
61
+ if (self + other + unknown !== frames) return undefined
62
+ const directionPresent = parts.length >= 5 ? asNonNegInt(parts[4]) : 0
63
+ if (directionPresent == null) return undefined
64
+ let directionLast: number | null = null
65
+ if (parts.length === 6 && parts[5] !== '') {
66
+ const last = Number(parts[5])
67
+ if (!Number.isFinite(last)) return undefined
68
+ directionLast = last
69
+ }
70
+ return {
71
+ schema: 1,
72
+ frames,
73
+ self,
74
+ other,
75
+ unknown,
76
+ majority: majorityOf(self, other, unknown, frames),
77
+ directionPresent,
78
+ directionLast,
79
+ }
80
+ }
81
+
82
+ export function parseEvenHubSpeakerRoleBody(raw: unknown): EvenSpeakerRoleHistogram | undefined {
83
+ if (!raw || typeof raw !== 'object') return undefined
84
+ const o = raw as Record<string, unknown>
85
+ const self = asNonNegInt(o.self)
86
+ const other = asNonNegInt(o.other)
87
+ const unknown = asNonNegInt(o.unknown)
88
+ const frames = asNonNegInt(o.frames)
89
+ if (self == null || other == null || unknown == null || frames == null) return undefined
90
+ if (self + other + unknown !== frames) return undefined
91
+ const directionPresent = o.directionPresent == null ? 0 : asNonNegInt(o.directionPresent)
92
+ if (directionPresent == null) return undefined
93
+ const directionLast = o.directionLast == null || o.directionLast === ''
94
+ ? null
95
+ : (typeof o.directionLast === 'number' && Number.isFinite(o.directionLast) ? o.directionLast : null)
96
+ return {
97
+ schema: 1,
98
+ frames,
99
+ self,
100
+ other,
101
+ unknown,
102
+ majority: majorityOf(self, other, unknown, frames),
103
+ directionPresent,
104
+ directionLast,
105
+ }
106
+ }
107
+
108
+ export function formatEvenRoleAgreement(opts: {
109
+ chunkIndex: number
110
+ even: EvenSpeakerRoleHistogram
111
+ amp: string
112
+ emb: string
113
+ similarity: number
114
+ }): string {
115
+ return `[even-role] chunk=${opts.chunkIndex} even=${opts.even.majority} amp=${opts.amp} emb=${opts.emb} sim=${opts.similarity.toFixed(2)} frames=${opts.even.frames}`
116
+ }
@@ -78,7 +78,6 @@ import {
78
78
  buildAttachedEnv,
79
79
  classifyStderr,
80
80
  extractNativeIdsFromLine,
81
- findBannedPermissionArg,
82
81
  isAttachedPermissionPolicy,
83
82
  resolveProviderBinary,
84
83
  type AttachedChildProcess,
@@ -86,6 +85,7 @@ import {
86
85
  type AttachedStderrClass,
87
86
  type BinaryResolution,
88
87
  } from './attached-provider-adapter.js'
88
+ import { findBannedPermissionArg } from './banned-permission-args.js'
89
89
 
90
90
  /**
91
91
  * Providers that can be forked. A strict subset of the attached set.
@@ -261,7 +261,7 @@ export function buildCodexForkArgs(nativeThreadId: string, cwd: string): string[
261
261
  /**
262
262
  * Build the argv, and REFUSE to hand back one carrying a banned permission flag.
263
263
  *
264
- * The predicate is IMPORTED from the adapter, not re-listed here. Two copies of a
264
+ * The predicate is IMPORTED from banned-permission-args, not re-listed here. Two copies of a
265
265
  * ban list in two modules is precisely the drift `native-thread-id.ts` was created
266
266
  * to end: the occupancy detector and the binding store each had their own idea of
267
267
  * what a thread id was, and a truncated id walked through the gap. A fork spawns a
@@ -6,6 +6,11 @@ import {
6
6
  isCursorProviderReady,
7
7
  resolveAgentBinary,
8
8
  } from './cursor-model-catalog.js'
9
+ import {
10
+ getOllamaCatalog,
11
+ getOllamaCatalogSnapshot,
12
+ isOllamaProviderReady,
13
+ } from './ollama-catalog.js'
9
14
 
10
15
  const DEFAULT_CACHE_TTL_MS = 30_000
11
16
  const PROBE_TIMEOUT_MS = 5_000
@@ -15,9 +20,11 @@ export interface HealthStaticProbeSnapshot {
15
20
  claude: string
16
21
  codex: string
17
22
  cursor: string
23
+ ollama: string
18
24
  claudeAvailable: boolean
19
25
  codexAvailable: boolean
20
26
  cursorAvailable: boolean
27
+ ollamaAvailable: boolean
21
28
  }
22
29
 
23
30
  interface CachedProbe<T> {
@@ -153,21 +160,36 @@ async function probeCursor(): Promise<{ value: string; available: boolean }> {
153
160
  }
154
161
  }
155
162
 
163
+ async function probeOllama(): Promise<{ value: string; available: boolean }> {
164
+ try {
165
+ const catalog = await getOllamaCatalog()
166
+ const available = isOllamaProviderReady()
167
+ if (available) return { value: catalog.model || 'available', available: true }
168
+ return { value: catalog.error || 'unavailable', available: false }
169
+ } catch {
170
+ const snapshot = getOllamaCatalogSnapshot()
171
+ return { value: snapshot.error || 'error', available: false }
172
+ }
173
+ }
174
+
156
175
  async function loadStaticHealthProbes(): Promise<HealthStaticProbeSnapshot> {
157
- const [python, claude, codex, cursor] = await Promise.all([
176
+ const [python, claude, codex, cursor, ollama] = await Promise.all([
158
177
  probePython(),
159
178
  probeClaude(),
160
179
  probeCodex(),
161
180
  probeCursor(),
181
+ probeOllama(),
162
182
  ])
163
183
  return {
164
184
  python,
165
185
  claude: claude.value,
166
186
  codex: codex.value,
167
187
  cursor: cursor.value,
188
+ ollama: ollama.value,
168
189
  claudeAvailable: claude.available,
169
190
  codexAvailable: codex.available,
170
191
  cursorAvailable: cursor.available,
192
+ ollamaAvailable: ollama.available,
171
193
  }
172
194
  }
173
195
 
@@ -1,6 +1,7 @@
1
1
  import { callClaudeStreaming, type CallOptions, type StreamCallbacks } from './claude-bridge.js'
2
2
  import { callCodexStreaming } from './codex-bridge.js'
3
3
  import { callCursorStreaming } from './cursor-bridge.js'
4
+ import { callOllamaStreaming } from './ollama-bridge.js'
4
5
  import {
5
6
  getOrCreateSession,
6
7
  getSessionModel,
@@ -13,6 +14,7 @@ import {
13
14
  isCodexModel,
14
15
  isClaudeModel,
15
16
  isCursorModel,
17
+ isOllamaModel,
16
18
  normalizeModelPreference,
17
19
  } from '../../shared/model-preference.js'
18
20
  import {
@@ -20,6 +22,10 @@ import {
20
22
  isCursorProviderReady,
21
23
  resolveCursorModelOption,
22
24
  } from './cursor-model-catalog.js'
25
+ import {
26
+ getOllamaCatalog,
27
+ isOllamaProviderReady,
28
+ } from './ollama-catalog.js'
23
29
  import type { ModelImageInput } from './model-image-input.js'
24
30
 
25
31
  // Bridges return as soon as their subprocess is spawned, while completion is
@@ -112,6 +118,14 @@ export async function callModelStreaming(
112
118
  }
113
119
  return await callCursorStreaming(query, sid, lockedCallbacks, resolvedModel, images, reference, globalMsgNum, options)
114
120
  }
121
+ if (isOllamaModel(resolvedModel)) {
122
+ await getOllamaCatalog()
123
+ if (!isOllamaProviderReady()) {
124
+ await lockedCallbacks.onError('ollama-bridge: Ollama is not running. Start ollama serve on this Mac.')
125
+ return sid
126
+ }
127
+ return await callOllamaStreaming(query, sid, lockedCallbacks, images, reference, globalMsgNum, options)
128
+ }
115
129
  if (isCodexModel(resolvedModel)) {
116
130
  return await callCodexStreaming(query, sid, lockedCallbacks, resolvedModel, images, reference, globalMsgNum, options)
117
131
  }