@gotcos/glasses-server 6.64.0 → 6.66.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/.env.example +5 -0
- package/CHANGELOG.md +28 -0
- package/README.md +32 -6
- package/bin/cli.cjs +20 -29
- package/bin/whisper-runtime.cjs +17 -0
- package/bin/whisper-runtime.d.cts +1 -0
- package/docs/work-intake.md +2 -0
- package/docs/work-task-runtime.md +11 -0
- package/package.json +5 -2
- package/server/index.ts +7 -0
- package/server/lib/dictation-clean-models.ts +3 -3
- package/server/lib/dictation-clean.ts +1 -1
- package/server/lib/health-static-probes.ts +4 -2
- package/server/lib/jev.ts +5 -3
- package/server/lib/task-dates.ts +369 -0
- package/server/lib/task-store.ts +30 -3
- package/server/lib/whisper-local.ts +4 -11
- package/server/lib/whisper-preview.ts +2 -2
- package/server/lib/work-search.ts +226 -0
- package/server/routes/jev.ts +6 -1
- package/server/routes/prompt-drafts.ts +1 -1
- package/server/routes/tasks.ts +3 -1
- package/server/routes/work-board.ts +7 -2
- package/server/routes/work-search.ts +72 -0
package/.env.example
CHANGED
|
@@ -340,3 +340,8 @@ BIND_HOST=0.0.0.0
|
|
|
340
340
|
# additionally requires a client that acknowledges displaying continue_note.
|
|
341
341
|
# Keep this in ~/.cos-glasses/.env; Control does not yet persist it on update.
|
|
342
342
|
COS_CONTINUE_FULL_PERMISSIONS=1
|
|
343
|
+
|
|
344
|
+
# Optional absolute executable paths for COS-managed whisper.cpp.
|
|
345
|
+
# Unset uses Homebrew then PATH. An invalid explicit path fails closed.
|
|
346
|
+
# COS_WHISPER_CLI_BIN=/absolute/path/to/whisper-cli
|
|
347
|
+
# COS_WHISPER_SERVER_BIN=/absolute/path/to/whisper-server
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
## 6.66.0 — 2026-10-08 — Managed voice setup and Haiku polish
|
|
2
|
+
|
|
3
|
+
- Advertise Haiku, Sonnet and GPT-5.6 Luna Fast. Default omitted-model polish to Haiku for lower cost, while honoring explicit model choices and retaining the transcript on failure.
|
|
4
|
+
- Glasses app 6.10.616 explicitly sends the chosen model and already works with server 6.65.0.
|
|
5
|
+
- Setup, live preview, committed transcription and HQ share explicit `COS_WHISPER_CLI_BIN` / `COS_WHISPER_SERVER_BIN` resolution. A broken explicit path fails closed; existing Homebrew/PATH detection remains the fallback.
|
|
6
|
+
- COS Control can prepare Turbo first for a local benchmark, then the chosen tier, without overwriting saved voice settings. Both preparation flags require `--setup-transcription --prepare-only`.
|
|
7
|
+
- Pairs with COS Control 0.5.273 and the signed/notarized whisper.cpp 1.9.1 runtime.
|
|
8
|
+
- Claude health probes resolve the installed absolute binary path, including managed-service environments without the interactive shell PATH.
|
|
9
|
+
|
|
10
|
+
## 6.65.0 — 2026-10-07 — Work search by meaning, and task dates
|
|
11
|
+
|
|
12
|
+
- `POST /api/work/search` `{ query, domain?, scope?, ids? }` finds the Work card the user means, not only the words they typed. Pairs with COS Control 0.5.259, which shows its own word matches at once and asks this after a pause.
|
|
13
|
+
- One Jev Choice per search over the board's own cards, plus `none`. The client names a view or exact card ids, never task text. A domain board searches that domain's cards, open and completed. All work searches open cards only, although Control's All work view also shows completed ones. Completed searches completed cards. Needs attention and In progress send their card ids.
|
|
14
|
+
- What goes to TypeSafe: each card's task text, on one line, at most 300 characters, without `[Work details](…)` links. URLs, email addresses and opaque ids (the shape of a key or a document id) are removed, credentials are masked by the server's secret redaction, and a markdown link keeps only its label. A bare domain such as bottlepos.com stays.
|
|
15
|
+
- The query is 2 to 200 characters, counted in code points, so 150 emoji are 150.
|
|
16
|
+
- The answer is `{ available: true, results: [{ id, p }], none, cached, tokens }`: cards at p 0.02 or more, most likely first, at most 20.
|
|
17
|
+
- When it cannot search it answers HTTP 200 with `{ available: false, reason, message }`. The reasons are `jev_not_configured`, `jev_key_rejected`, `jev_cap_reached`, `jev_breaker_open`, `jev_request_rejected` (Jev refused the request: a 400, 413 or 422), `jev_unavailable` (no answer, a 5xx, or a malformed answer), `search_off` and `too_many_candidates`. A Choice holds 254 cards, so a bigger set is refused, never cut; Completed across all domains is 198 cards today. Control keeps its word search in every case. The server logs each reason at most once per 10 minutes.
|
|
18
|
+
- Its own cost controls, apart from Work intake and session advice. A daily cap of 1,000,000 input tokens (`COS_JEV_SEARCH_DAILY_TOKENS`), about 110 to 200 searches by board size, with its own ledger (`<data>/jev-search-usage.json`); it resets at midnight UTC. Its own breaker: 3 failures, then an hour; a refused request does not count, because the same cards would be refused again. A switch, `COS_JEV_SEARCH=0`. Both come from the server's environment, so changing them takes a restart. Saving a new TypeSafe key clears this breaker too. `COS_JEV_DAILY_TOKENS` is unchanged.
|
|
19
|
+
- The same query over the same cards is answered from memory for 10 minutes (up to 200 searches).
|
|
20
|
+
- Search is read-only, so it does not take the server's request lease: an update or restart never waits on a search, and a search during one is answered rather than refused.
|
|
21
|
+
- Measured on 2026-10-06 on the live board (293 cards, 95 open): 4 searches, the right card first at p 0.99 for each of 3 real queries, `none` at 1 for an unrelated one, 4,815 to 4,963 input tokens and 268 to 431 ms each including the board read. The repeat came from the cache with no call.
|
|
22
|
+
- `GET /api/tasks` and `GET /api/work-board` rows carry `createdOn` (`YYYY-MM-DD` or null), `createdFrom` (`source`, `git` or null) and `lineChangedAt` (ISO time or null), for Control's date orders. Control reads `/api/work-board` for its Work board, so both routes carry them. Additive: every existing field and the task revision are unchanged, and the fields are present as null when unknown, so a client can tell this server from an older one.
|
|
23
|
+
- `createdOn` is the first date in the task's source label, usually the meeting date. Otherwise it is the day the task's words first appeared in the git history of its `tasks.md`, cached on disk (`<data>/task-created-dates.json`, at most 5,000) by file and a hash of the words. A lookup that times out or fails is not cached as "not in history"; it is tried again after 10 minutes.
|
|
24
|
+
- `lineChangedAt` is when git last changed the task's line (blame, once per HEAD and file change). A line that is not committed yet is null, never "now". tasks.md is committed by the COS sync every one to three days, so this is later than the real change.
|
|
25
|
+
- Every read of either route, the phone's and the glasses' included, keeps all of its git work inside 1.5 s, half the phone's 4 s timeout less an allowance for the board read. Past that the dates still unknown come back null and the git work finishes in the background for a later read.
|
|
26
|
+
- Read-only: nothing is written into tasks.md. Git runs only when a `.git` exists at or above the `tasks.md` folder. A public install keeps its tasks outside any repository, so no git process starts. If the home folder is itself a git repository, git does run, finds `tasks.md` untracked, and the git fields stay null. Both the COS bridge and the packaged task runtime get the fields.
|
|
27
|
+
- Measured on 2026-10-06 on the live board (293 cards, a read-only scratch run): 241 dated from the source label, 52 from git, none without a date; 278 with `lineChangedAt` (the other 15 are not committed yet). The first call took 1.2 s (68 git lookups), later calls about 20 ms, and a restart 74 ms with no git history lookup.
|
|
28
|
+
|
|
1
29
|
## 6.64.0 — 2026-10-06 — Meeting context keys
|
|
2
30
|
|
|
3
31
|
- Meeting rows and details carry `contextKeys`, `contextSupported`, and, when files can't be added, `contextReason` (plus `contextAmbiguous`). The fields are additive and read-only. They are the keys COS Control 0.5.258 files a meeting's dropped screenshots, slides and documents under, so the files go to every Work card linked to the meeting.
|
package/README.md
CHANGED
|
@@ -646,18 +646,24 @@ and counts only, never a speaker name, path or error text; those stay in
|
|
|
646
646
|
|
|
647
647
|
## Final dictation polish (6.61.5)
|
|
648
648
|
|
|
649
|
-
With glasses app 6.10.
|
|
650
|
-
(the default,
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
649
|
+
With glasses app 6.10.616, **Settings → Voice → Polish model** offers **Haiku 5.5**
|
|
650
|
+
(the lower-cost default), **Sonnet 5.5** (higher-quality polish), and **GPT-5.6 Luna Fast**.
|
|
651
|
+
Haiku and Sonnet use your Claude CLI sign-in; Luna uses your Cursor CLI sign-in.
|
|
652
|
+
Luna uses `gpt-5.6-luna-none-fast` and needs server 6.61.5. Existing explicit choices
|
|
653
|
+
are preserved. Unset or unknown app preferences use Haiku. Both phone text and
|
|
654
|
+
recovered audio drafts honor the choice. Existing server 6.65.0 already accepts
|
|
655
|
+
explicit Haiku requests, so the new app does not require a server update.
|
|
656
|
+
|
|
657
|
+
The next server release also defaults omitted-model requests to Haiku and advertises
|
|
658
|
+
all three options in `capabilities.dictationCleanup.models`. Until that release,
|
|
659
|
+
6.65.0 still defaults requests without a model to Sonnet; 6.10.616 sends its choice
|
|
660
|
+
explicitly. This is a polish-model change, not a change to the speech recognition engine.
|
|
654
661
|
|
|
655
662
|
Cleanup uses subscription CLIs only, with no API-key fallback or automatic model
|
|
656
663
|
substitution. A failed or unavailable cleanup keeps the glossary-corrected
|
|
657
664
|
transcript. Turn **Final polish (AI)** off to skip the model call. Claude tools
|
|
658
665
|
and hooks are disabled; Cursor runs in read-only ask mode with a temporary profile
|
|
659
666
|
and empty workspace. Live preview and canonical meeting transcription are unchanged.
|
|
660
|
-
Older clients may still explicitly request Haiku.
|
|
661
667
|
|
|
662
668
|
## HQ dictation
|
|
663
669
|
|
|
@@ -917,3 +923,23 @@ This is a local owner instance, separate from app.gotcos.com tenants and hosted
|
|
|
917
923
|
## Provider parity in 6.62.0
|
|
918
924
|
|
|
919
925
|
See [provider capabilities and limits](docs/provider-parity.md) for Continue permissions, client compatibility, hook trust, observation limits and rollback. Glasses 6.10.600 shows the server's Continue note. Older clients retain Cursor Ask behavior; forks remain read-only.
|
|
926
|
+
|
|
927
|
+
### Managed Whisper runtime (6.66.0 candidate)
|
|
928
|
+
|
|
929
|
+
COS Control 0.5.272 prepares a signed, notarized whisper.cpp 1.9.1 runtime during
|
|
930
|
+
voice setup on Apple silicon. It supplies absolute `COS_WHISPER_CLI_BIN` and
|
|
931
|
+
`COS_WHISPER_SERVER_BIN` paths to the server; Homebrew is not needed for that flow.
|
|
932
|
+
Direct server installs still fall back to Homebrew and then PATH when overrides
|
|
933
|
+
are unset. A configured but missing/non-executable override is an error, never a
|
|
934
|
+
silent change of runtime.
|
|
935
|
+
|
|
936
|
+
`--setup-transcription --prepare-only --voice-benchmark-prepare` prepares Turbo
|
|
937
|
+
without changing saved tier keys. Control measures the device and follows with
|
|
938
|
+
`--setup-transcription --prepare-only --transcription-tier balanced|max
|
|
939
|
+
--preserve-voice-settings`. The latter prepares all required models, including
|
|
940
|
+
Large-v3 for saved-meeting HQ in both tiers. Applying a tier remains Control's
|
|
941
|
+
transactional action. Downloading or cancelling preparation does not change it.
|
|
942
|
+
Runtime/model setup is local; the existing provider sign-in requirements remain.
|
|
943
|
+
|
|
944
|
+
Candidate only: publish the versioned runtime asset and server package before
|
|
945
|
+
promoting the matching Control appcast entry.
|
package/bin/cli.cjs
CHANGED
|
@@ -25,7 +25,12 @@ const { homedir } = require('os')
|
|
|
25
25
|
const PKG_ROOT = resolve(__dirname, '..')
|
|
26
26
|
const CONFIG_DIR = join(homedir(), '.cos-glasses')
|
|
27
27
|
const PREPARE_ONLY = process.argv.includes('--prepare-only')
|
|
28
|
+
const VOICE_BENCHMARK_PREPARE = process.argv.includes('--voice-benchmark-prepare')
|
|
28
29
|
const SETUP_TRANSCRIPTION = process.argv.includes('--setup-transcription')
|
|
30
|
+
if ((VOICE_BENCHMARK_PREPARE || process.argv.includes('--preserve-voice-settings')) && !(SETUP_TRANSCRIPTION && PREPARE_ONLY)) {
|
|
31
|
+
console.error('Voice preparation flags require --setup-transcription --prepare-only.');
|
|
32
|
+
process.exit(64)
|
|
33
|
+
}
|
|
29
34
|
const SETUP_SPEAKER_MODEL = process.argv.includes('--setup-speaker-model')
|
|
30
35
|
const HOOKS_ACTION = process.argv.includes('--hooks') ? process.argv[process.argv.indexOf('--hooks') + 1] : undefined
|
|
31
36
|
function optionValue(name) {
|
|
@@ -540,12 +545,15 @@ function upsertEnvValue(file, key, value) {
|
|
|
540
545
|
chmodSync(file, 0o600)
|
|
541
546
|
}
|
|
542
547
|
|
|
543
|
-
if (SETUP_TRANSCRIPTION) {
|
|
548
|
+
if (SETUP_TRANSCRIPTION && !VOICE_BENCHMARK_PREPARE) {
|
|
549
|
+
const preserveVoiceSettings = process.argv.includes('--preserve-voice-settings')
|
|
544
550
|
const previewModel = TRANSCRIPTION_TIER === 'max' ? 'turbo' : 'small.en'
|
|
545
551
|
const commitModel = TRANSCRIPTION_TIER === 'max' ? 'large-v3' : 'turbo'
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
552
|
+
if (!preserveVoiceSettings) {
|
|
553
|
+
upsertEnvValue(ENV_FILE, 'COS_WHISPER_TRANSCRIPTION_TIER', TRANSCRIPTION_TIER)
|
|
554
|
+
upsertEnvValue(ENV_FILE, 'COS_WHISPER_PREVIEW_MODEL', previewModel)
|
|
555
|
+
upsertEnvValue(ENV_FILE, 'COS_WHISPER_COMMIT_MODEL', commitModel)
|
|
556
|
+
}
|
|
549
557
|
process.env.COS_WHISPER_TRANSCRIPTION_TIER = TRANSCRIPTION_TIER
|
|
550
558
|
process.env.COS_WHISPER_PREVIEW_MODEL = previewModel
|
|
551
559
|
process.env.COS_WHISPER_COMMIT_MODEL = commitModel
|
|
@@ -573,8 +581,7 @@ if (!process.env.COS_PROFILE_PATH) process.env.COS_PROFILE_PATH = PROFILE_FILE
|
|
|
573
581
|
|
|
574
582
|
// Step 5: local Whisper detection + model download. Voice stays local-only by
|
|
575
583
|
// default; cloud fallback requires an explicit flag plus a configured key.
|
|
576
|
-
const
|
|
577
|
-
const WHISPER_SERVER_KNOWN_PATHS = ['/opt/homebrew/bin/whisper-server', '/usr/local/bin/whisper-server']
|
|
584
|
+
const { resolveWhisperBinary } = require('./whisper-runtime.cjs')
|
|
578
585
|
const WHISPER_MODEL_DIR = join(homedir(), '.local/share/whisper-models')
|
|
579
586
|
const WHISPER_MODEL_PATH = join(WHISPER_MODEL_DIR, 'ggml-large-v3-turbo.bin')
|
|
580
587
|
const WHISPER_MODEL_PARTIAL = WHISPER_MODEL_PATH + '.partial'
|
|
@@ -591,24 +598,8 @@ const WHISPER_LARGE_MODEL_MIN_BYTES = 2_800_000_000
|
|
|
591
598
|
const WHISPER_MODEL_EXPECTED_BYTES = 1_624_555_275
|
|
592
599
|
const WHISPER_SMALL_MODEL_EXPECTED_BYTES = 487_614_201
|
|
593
600
|
const WHISPER_LARGE_MODEL_EXPECTED_BYTES = 3_095_033_483
|
|
594
|
-
function findWhisperCli() {
|
|
595
|
-
|
|
596
|
-
try {
|
|
597
|
-
const found = execSync('command -v whisper-cli 2>/dev/null', { shell: '/bin/sh', stdio: 'pipe', timeout: 2000 }).toString().trim()
|
|
598
|
-
return found || null
|
|
599
|
-
} catch {
|
|
600
|
-
return null
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
function findWhisperServer() {
|
|
604
|
-
for (const p of WHISPER_SERVER_KNOWN_PATHS) { if (existsSync(p)) return p }
|
|
605
|
-
try {
|
|
606
|
-
const found = execSync('command -v whisper-server 2>/dev/null', { shell: '/bin/sh', stdio: 'pipe', timeout: 2000 }).toString().trim()
|
|
607
|
-
return found || null
|
|
608
|
-
} catch {
|
|
609
|
-
return null
|
|
610
|
-
}
|
|
611
|
-
}
|
|
601
|
+
function findWhisperCli() { return resolveWhisperBinary('whisper-cli') }
|
|
602
|
+
function findWhisperServer() { return resolveWhisperBinary('whisper-server') }
|
|
612
603
|
function isValidWhisperModel(p, minBytes = WHISPER_MODEL_MIN_BYTES) {
|
|
613
604
|
if (!existsSync(p)) return false
|
|
614
605
|
try { return statSync(p).size >= minBytes } catch { return false }
|
|
@@ -652,10 +643,10 @@ if (SETUP_TRANSCRIPTION && process.env.SKIP_WHISPER_DOWNLOAD !== '1') {
|
|
|
652
643
|
mkdirSync(WHISPER_MODEL_DIR, { recursive: true })
|
|
653
644
|
const targets = [
|
|
654
645
|
[WHISPER_MODEL_PATH, WHISPER_MODEL_PARTIAL, WHISPER_MODEL_MIN_BYTES, WHISPER_MODEL_EXPECTED_BYTES],
|
|
655
|
-
...(TRANSCRIPTION_TIER === 'balanced'
|
|
646
|
+
...(!VOICE_BENCHMARK_PREPARE && TRANSCRIPTION_TIER === 'balanced'
|
|
656
647
|
? [[WHISPER_SMALL_MODEL_PATH, WHISPER_SMALL_MODEL_PARTIAL, WHISPER_SMALL_MODEL_MIN_BYTES, WHISPER_SMALL_MODEL_EXPECTED_BYTES]]
|
|
657
648
|
: []),
|
|
658
|
-
[WHISPER_LARGE_MODEL_PATH, WHISPER_LARGE_MODEL_PARTIAL, WHISPER_LARGE_MODEL_MIN_BYTES, WHISPER_LARGE_MODEL_EXPECTED_BYTES],
|
|
649
|
+
...(!VOICE_BENCHMARK_PREPARE ? [[WHISPER_LARGE_MODEL_PATH, WHISPER_LARGE_MODEL_PARTIAL, WHISPER_LARGE_MODEL_MIN_BYTES, WHISPER_LARGE_MODEL_EXPECTED_BYTES]] : []),
|
|
659
650
|
]
|
|
660
651
|
const missingBytes = targets.reduce((sum, [path, , minimum, expected]) =>
|
|
661
652
|
sum + (isValidWhisperModel(path, minimum) ? 0 : expected), 0)
|
|
@@ -716,7 +707,7 @@ if (process.env.COS_OPENAI_WHISPER_FALLBACK === '1') {
|
|
|
716
707
|
|
|
717
708
|
const previewChoice = (process.env.COS_WHISPER_PREVIEW_MODEL || process.env.COS_WHISPER_REALTIME_MODEL || 'auto').trim().toLowerCase()
|
|
718
709
|
const wantsSmallPreview = ['small', 'small.en', 'ggml-small.en.bin'].includes(previewChoice)
|
|
719
|
-
if (whisperCliPath && wantsSmallPreview) {
|
|
710
|
+
if (whisperCliPath && wantsSmallPreview && !VOICE_BENCHMARK_PREPARE) {
|
|
720
711
|
if (isValidWhisperModel(WHISPER_SMALL_MODEL_PATH, WHISPER_SMALL_MODEL_MIN_BYTES)) {
|
|
721
712
|
console.log(green(' ✓') + ' Small.en preview model ready ' + dim('— Turbo remains authoritative'))
|
|
722
713
|
} else {
|
|
@@ -745,7 +736,7 @@ if (whisperCliPath && wantsSmallPreview) {
|
|
|
745
736
|
}
|
|
746
737
|
}
|
|
747
738
|
|
|
748
|
-
if (whisperCliPath && SETUP_TRANSCRIPTION) {
|
|
739
|
+
if (whisperCliPath && SETUP_TRANSCRIPTION && !VOICE_BENCHMARK_PREPARE) {
|
|
749
740
|
if (isValidWhisperModel(WHISPER_LARGE_MODEL_PATH, WHISPER_LARGE_MODEL_MIN_BYTES)) {
|
|
750
741
|
const largeRole = TRANSCRIPTION_TIER === 'max'
|
|
751
742
|
? 'live preview + commit and saved-meeting polish'
|
|
@@ -789,7 +780,7 @@ if (whisperCliPath && SETUP_TRANSCRIPTION) {
|
|
|
789
780
|
// -- sixty times larger -- downloaded here with SKIP_WHISPER_DOWNLOAD as the
|
|
790
781
|
// escape hatch. Same shape, same opt-out, so nobody on a metered or restricted
|
|
791
782
|
// network is forced into it.
|
|
792
|
-
if (!SETUP_SPEAKER_MODEL && process.env.SKIP_SPEAKER_MODEL_DOWNLOAD !== '1') {
|
|
783
|
+
if (!VOICE_BENCHMARK_PREPARE && !SETUP_SPEAKER_MODEL && process.env.SKIP_SPEAKER_MODEL_DOWNLOAD !== '1') {
|
|
793
784
|
const spkDest = join(CONFIG_DIR, 'models', SPEAKER_MODEL.filename)
|
|
794
785
|
if (existsSync(spkDest) && sha256File(spkDest) === SPEAKER_MODEL.sha256) {
|
|
795
786
|
console.log(green(' ✓') + ' Voiceprint model ready ' + dim('— named speakers available'))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Shared by setup and both live Whisper workers. Explicit overrides fail closed.
|
|
2
|
+
const { accessSync, constants, statSync } = require('node:fs')
|
|
3
|
+
const { isAbsolute, join } = require('node:path')
|
|
4
|
+
function executable(path) {
|
|
5
|
+
try { accessSync(path, constants.X_OK); return statSync(path).isFile() } catch { return false }
|
|
6
|
+
}
|
|
7
|
+
function resolveWhisperBinary(name, env = process.env, available = executable) {
|
|
8
|
+
if (!['whisper-cli', 'whisper-server'].includes(name)) throw new Error('Unknown Whisper binary')
|
|
9
|
+
const key = name === 'whisper-cli' ? 'COS_WHISPER_CLI_BIN' : 'COS_WHISPER_SERVER_BIN'
|
|
10
|
+
if (env[key] !== undefined && env[key] !== '') {
|
|
11
|
+
const path = env[key].trim()
|
|
12
|
+
return isAbsolute(path) && available(path) ? path : null
|
|
13
|
+
}
|
|
14
|
+
const candidates = ['/opt/homebrew/bin', '/usr/local/bin', ...(env.PATH || '').split(':')]
|
|
15
|
+
return candidates.filter(isAbsolute).map(p => join(p, name)).find(available) || null
|
|
16
|
+
}
|
|
17
|
+
module.exports = { resolveWhisperBinary }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function resolveWhisperBinary(name: 'whisper-cli' | 'whisper-server', env?: NodeJS.ProcessEnv, available?: (path: string) => boolean): string | null
|
package/docs/work-intake.md
CHANGED
|
@@ -35,6 +35,8 @@ Since 6.57.1 `POST /api/work-board/session-recommendation` also takes exactly `{
|
|
|
35
35
|
|
|
36
36
|
Since 6.58.0 `POST /api/work-board/completion-check` takes exactly `{ domain, id, provider, sessionId }` plus an optional `after` (ISO time). It asks Jev whether the session's replies since `after` (or its newest reply) show the task finished, judged against its Done when or, without one, its own text. The server reads the task from the board and the replies from its own transcripts; the client sends names and a time only. It answers `{ provider: "jev", verdict: "done" | "not_done" | "unclear", confidence, basis: "done_when" | "task", model, cached }` or `{ provider: "none", reason }` (`no_reply`, `no_task_text`, a Jev condition, `completion_unavailable`). Errors: 400 `invalid_completion_request`, 404 `task_not_found`, 404 `session_not_found`. COS Control 0.5.247 asks only while the session is idle and its newest reply since the handoff has no `COS-WORK` status line, at most twice per handoff, and moves a card to QA only on done at 0.8 (0.85 without a Done when).
|
|
37
37
|
|
|
38
|
+
Since 6.65.0 `POST /api/work/search` takes `{ query, domain?, scope?, ids? }`: a query of 2 to 200 characters (code points), an optional domain, an optional scope (`all`, `attention`, `progress` or `completed`) and optional card ids (12 hex). It asks Jev one Choice over the board's own cards, plus `none`, and answers `{ available: true, results: [{ id, p }], none, cached, tokens }` with cards at p 0.02 or more, most likely first, at most 20. Exact ids win (Control sends them for Needs attention and In progress). Otherwise a domain board covers that domain's cards, All work its open cards, and Completed its completed cards. Each card goes to TypeSafe as its task text without `[Work details](…)` links, URLs, email addresses or opaque ids, with credentials masked by the server's secret redaction, at most 300 characters. A search that cannot run answers 200 with `{ available: false, reason, message }`: `jev_not_configured`, `jev_key_rejected`, `jev_cap_reached`, `jev_breaker_open`, `jev_request_rejected` (a 400, 413 or 422 from Jev; not counted toward the breaker), `jev_unavailable`, `search_off` (`COS_JEV_SEARCH=0`) or `too_many_candidates` (more than 254 cards). Errors: 400 `invalid_search_request`, 503 `work_board_unavailable`. Search has its own daily cap (`COS_JEV_SEARCH_DAILY_TOKENS`, default 1,000,000, reset at midnight UTC), ledger (`data/jev-search-usage.json`) and breaker, so it never spends the cap Work intake and session advice share; the switch and the cap are read from the environment set at start, so a change takes a restart. The same query over the same cards is answered from memory for 10 minutes. The route is read-only and does not take the server's request lease.
|
|
39
|
+
|
|
38
40
|
Since 6.58.2 `POST /api/query-jobs` also takes an optional `sessionName` (a string; the server cleans it as the 6.58.2 changelog says and keeps at most 100 characters; anything else is no name, never an error). It names only a Claude session the job starts itself (`claude --name=<name>`, when the installed CLI lists `--name`); a Continue, a dispatch, a Codex, Cursor or Ollama job, and an older server ignore it, and it is not part of the request fingerprint. A Claude job's snapshot carries `cliSessionId` from Claude's first stream event while the job is still `running`, not only at completion. COS Control 0.5.250 sends the Work item's title for New session, Start work and Fork to Claude, and links its receipt as soon as the running job names the session.
|
|
39
41
|
|
|
40
42
|
Since 6.59.0 `GET /api/work-board/activity` also projects each handoff's progress (the report, its evidence of at most 280 characters, the newest move), the task's own revision and a saved destination, `GET /api/work-board/activity/open` lists every open handoff across the board, and `POST /api/work-board/handoff-requests` lets the glasses ask COS Control to start Work, reply, or say it is not done yet, which Control claims on this Mac and sends with its own guards. The server runs no provider and no Jev for any of it. See `docs/work-handoff-requests.md`.
|
|
@@ -30,6 +30,17 @@ single-command child pins its domain as cwd instead of relying on unsupported
|
|
|
30
30
|
atomic replacement writes use exclusive no-follow temporary files. Same-user
|
|
31
31
|
modification of the installed package/manifest is outside this integrity boundary.
|
|
32
32
|
|
|
33
|
+
Since 6.65.0 `GET /api/tasks` and `GET /api/work-board` rows also carry `createdOn`,
|
|
34
|
+
`createdFrom` and `lineChangedAt`, for both the COS bridge and this runtime. The server reads them; the
|
|
35
|
+
runtime does not. `createdOn` is the first `YYYY-MM-DD` in the source label, else the
|
|
36
|
+
day the task's words first appeared in the git history of its `tasks.md`.
|
|
37
|
+
`lineChangedAt` is the git blame time of a committed line. Each is null when unknown,
|
|
38
|
+
including every git-derived value when no git repository tracks the `tasks.md`.
|
|
39
|
+
All git work for one read (rev-parse, blame, first-seen lookups) shares a 1.5 s
|
|
40
|
+
deadline; what is still unknown then comes back null, and the git work finishes in the
|
|
41
|
+
background for a later read. Git runs only when a `.git` exists at or above the
|
|
42
|
+
`tasks.md` folder. Nothing is written into `tasks.md`.
|
|
43
|
+
|
|
33
44
|
The default lock namespace remains `~/Library/Application Support/COS/.task_locks.json`
|
|
34
45
|
and canonical `file:<domain>` entries. `COS_TASK_LOCK_STORE` is an explicit override
|
|
35
46
|
for isolated fixtures. Existing COS bridge calls keep their original environment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.66.0",
|
|
4
4
|
"description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"!server/scripts/work-review-mutation-gate.mjs",
|
|
47
47
|
"!server/scripts/work-intake-mutation-gate.mjs",
|
|
48
48
|
"!server/scripts/work-progress-mutation-gate.mjs",
|
|
49
|
+
"!server/scripts/work-search-mutation-gate.mjs",
|
|
49
50
|
"!server/scripts/away-hold-mutation-gate.mjs",
|
|
50
51
|
"!server/scripts/nemotron-mutation-gate.mjs",
|
|
51
52
|
"!server/scripts/merged-scribe-mutation-gate.mjs",
|
|
@@ -76,7 +77,9 @@
|
|
|
76
77
|
"docs/work-task-runtime.md",
|
|
77
78
|
"!server/scripts/build-work-task-runtime.py",
|
|
78
79
|
"bin/hooks/cos-cursor-observer.cjs",
|
|
79
|
-
"docs/provider-parity.md"
|
|
80
|
+
"docs/provider-parity.md",
|
|
81
|
+
"bin/whisper-runtime.cjs",
|
|
82
|
+
"bin/whisper-runtime.d.cts"
|
|
80
83
|
],
|
|
81
84
|
"author": "COS Contributors",
|
|
82
85
|
"license": "MIT",
|
package/server/index.ts
CHANGED
|
@@ -142,6 +142,8 @@ import { createWorkBoardRouter } from './routes/work-board.js'
|
|
|
142
142
|
import { createWorkIntakeRouter } from './routes/work-intake.js'
|
|
143
143
|
import { createWorkHandoffRequestsRouter } from './routes/work-handoff-requests.js'
|
|
144
144
|
import { createJevRouter } from './routes/jev.js'
|
|
145
|
+
import { createWorkSearchRouter } from './routes/work-search.js'
|
|
146
|
+
import { isReadOnlyApiPost } from './lib/work-search.js'
|
|
145
147
|
import { dataPath } from './lib/data-dir.js'
|
|
146
148
|
import { loadJsonOrQuarantine, durableAtomicWriteFileSync } from './lib/atomic-fs.js'
|
|
147
149
|
import { WorkIntakeStore, createOptionalWorkIntakeStore } from './lib/work-intake-store.js'
|
|
@@ -324,6 +326,9 @@ app.use('/api', requireApiToken(API_TOKEN))
|
|
|
324
326
|
// through their true terminal boundary.
|
|
325
327
|
app.use('/api', (req, res, next) => {
|
|
326
328
|
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next()
|
|
329
|
+
// 6.65.0: a POST that only reads (Work search: a board read and one Jev question) is not a mutation. See
|
|
330
|
+
// lib/work-search.ts isReadOnlyApiPost for why holding the lease would hurt and why skipping it is safe.
|
|
331
|
+
if (isReadOnlyApiPost(req.method, req.path)) return next()
|
|
327
332
|
// V2 video original chunks are bounded to the advertised session size (1 MiB
|
|
328
333
|
// on new sessions, 256 KiB on leftover drafts) and commit through the upload
|
|
329
334
|
// registry's own generation lock. Holding the global mutation lease while
|
|
@@ -756,6 +761,8 @@ app.use('/api', createWorkIntakeRouter({ store: workIntakeStore }))
|
|
|
756
761
|
// Jev key (Control Settings), Continue/Fork/New session recommendations for Work tasks (6.57.0) and meeting reviews
|
|
757
762
|
// (6.57.1), and the Work completion check (6.58.0).
|
|
758
763
|
app.use('/api', createJevRouter(workReviewRuntime ? { review: async id => workReviewRuntime.peek(id) } : {}))
|
|
764
|
+
// Work search by meaning (6.65.0, Control 0.5.259): one Jev Choice over the board's cards, with its own cap, switch and breaker.
|
|
765
|
+
app.use('/api', createWorkSearchRouter())
|
|
759
766
|
app.use('/api', queryRouter)
|
|
760
767
|
// The scheduled start-of-day brief: settings, status, run-now. Same auth as
|
|
761
768
|
// every other settings route; the brief itself is an ordinary durable job.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** Public choices. Haiku
|
|
2
|
-
export const DICTATION_CLEAN_MODELS = ['sonnet', 'luna-5.6-fast'] as const
|
|
3
|
-
export type DictationCleanModel = typeof DICTATION_CLEAN_MODELS[number]
|
|
1
|
+
/** Public polish choices. Haiku is the cost-first default. */
|
|
2
|
+
export const DICTATION_CLEAN_MODELS = ['haiku', 'sonnet', 'luna-5.6-fast'] as const
|
|
3
|
+
export type DictationCleanModel = typeof DICTATION_CLEAN_MODELS[number]
|
|
4
4
|
export const LUNA_DICTATION_CLI_MODEL = 'gpt-5.6-luna-none-fast'
|
|
5
5
|
export function isDictationCleanModel(value: unknown): value is DictationCleanModel {
|
|
6
6
|
return value === 'sonnet' || value === 'luna-5.6-fast' || value === 'haiku'
|
|
@@ -32,7 +32,7 @@ export async function autoCleanDictation(
|
|
|
32
32
|
opts: { model?: string; signal?: AbortSignal } = {},
|
|
33
33
|
): Promise<string> {
|
|
34
34
|
if (opts.signal?.aborted) throw new Error('Auto-clean aborted')
|
|
35
|
-
const model = (opts.model || process.env.COS_DICTATION_AUTOCLEAN_MODEL || '
|
|
35
|
+
const model = (opts.model || process.env.COS_DICTATION_AUTOCLEAN_MODEL || 'haiku').toLowerCase()
|
|
36
36
|
if (!isDictationCleanModel(model)) throw new Error('Unsupported dictation cleanup model')
|
|
37
37
|
const cursor = model === 'luna-5.6-fast'
|
|
38
38
|
const binary = resolveProviderBinary(cursor ? 'cursor' : 'claude')
|
|
@@ -123,9 +123,11 @@ async function probePython(): Promise<string> {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async function probeClaude(): Promise<{ value: string; available: boolean }> {
|
|
126
|
+
export async function probeClaude(): Promise<{ value: string; available: boolean }> {
|
|
127
127
|
try {
|
|
128
|
-
const
|
|
128
|
+
const resolved = resolveProviderBinary('claude')
|
|
129
|
+
if (!resolved.ok) return { value: `unresolved (${resolved.detail})`, available: false }
|
|
130
|
+
const result = await execute(resolved.path, ['--version'])
|
|
129
131
|
return { value: firstNonemptyLine(result.stdout) ?? 'available', available: true }
|
|
130
132
|
} catch {
|
|
131
133
|
return { value: 'error', available: false }
|
package/server/lib/jev.ts
CHANGED
|
@@ -88,8 +88,10 @@ export class JevClient {
|
|
|
88
88
|
private failures = 0
|
|
89
89
|
private breakerUntil = 0
|
|
90
90
|
private lastError: string | null = null
|
|
91
|
+
/** `cap` is the daily input-token cap for THIS client's ledger. Work search (6.65.0) passes its own; the default is
|
|
92
|
+
* COS_JEV_DAILY_TOKENS, shared by Work intake advice, session recommendations and completion checks. */
|
|
91
93
|
constructor(private readonly fetchImpl: Fetch = fetch, private readonly now: () => Date = () => new Date(),
|
|
92
|
-
private readonly usageFile = JEV_USAGE_FILE) {}
|
|
94
|
+
private readonly usageFile = JEV_USAGE_FILE, private readonly cap: () => number = dailyCap) {}
|
|
93
95
|
|
|
94
96
|
usedToday(): number {
|
|
95
97
|
try {
|
|
@@ -121,7 +123,7 @@ export class JevClient {
|
|
|
121
123
|
status(): { configured: boolean; source: JevKeySource; savedAt?: string; validatedAt?: string; usedToday: number; dailyCap: number; breakerOpenUntil: string | null; lastError: string | null } {
|
|
122
124
|
const key = resolveJevKey()
|
|
123
125
|
return { configured: !!key, source: key?.source ?? 'none', savedAt: key?.savedAt, validatedAt: key?.validatedAt,
|
|
124
|
-
usedToday: this.usedToday(), dailyCap:
|
|
126
|
+
usedToday: this.usedToday(), dailyCap: this.cap(),
|
|
125
127
|
breakerOpenUntil: this.breakerUntil > this.now().getTime() ? new Date(this.breakerUntil).toISOString() : null, lastError: this.lastError }
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -130,7 +132,7 @@ export class JevClient {
|
|
|
130
132
|
const key = resolveJevKey()
|
|
131
133
|
if (!key) throw new JevError('jev_not_configured')
|
|
132
134
|
if (this.breakerUntil > this.now().getTime()) throw new JevError('jev_breaker_open')
|
|
133
|
-
if (this.usedToday() + estimate >
|
|
135
|
+
if (this.usedToday() + estimate > this.cap()) this.fail('jev_cap_reached', false)
|
|
134
136
|
let res: Response
|
|
135
137
|
try {
|
|
136
138
|
res = await this.fetchImpl(`${JEV_ENDPOINT}/systemone`, { method: 'POST', signal: AbortSignal.timeout(JEV_LIMITS.timeoutMs),
|