@gotcos/glasses-server 7.0.31 → 7.0.34
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 +10 -8
- package/CHANGELOG.md +161 -0
- package/bin/hooks/cos-session-hook +3 -2
- package/docs/pairing-fixture.json +1 -1
- package/package.json +3 -1
- package/server/index.ts +4 -2
- package/server/lib/api-auth.ts +3 -2
- package/server/lib/archive.ts +4 -2
- package/server/lib/claude-hooks-installer.ts +101 -15
- package/server/lib/codex-hooks-installer.ts +4 -1
- package/server/lib/full-hold.ts +61 -5
- package/server/lib/glasses-pairing.ts +2 -1
- package/server/lib/permission-broker.ts +130 -14
- package/server/lib/question-hold-setting.ts +38 -0
- package/server/lib/question-queue.ts +111 -0
- package/server/lib/session-hooks-runtime.ts +28 -3
- package/server/lib/session-title-cache.ts +40 -0
- package/server/lib/turn-end-ask.ts +51 -1
- package/server/lib/web-session-auth.ts +17 -0
- package/server/routes/agent-sessions.ts +3 -0
- package/server/routes/permission-broker.ts +31 -2
- package/server/routes/session-hooks.ts +23 -0
- package/server/routes/sessions.ts +3 -0
- package/server/web-control/.manifest.json +9 -9
- package/server/web-control/app.css +26 -3
- package/server/web-control/app.js +234 -35
- package/server/web-control/control.css +5 -1
- package/server/web-control/index.html +7 -5
- package/server/web-control/panes/memories.js +18 -9
- package/server/web-control/panes/messages.js +15 -3
- package/server/web-control/panes/questions.js +6 -1
- package/server/web-control/panes/settings.js +18 -10
- package/server/web-control/shell.js +8 -13
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
|
|
51
51
|
import express, { Router, type NextFunction, type Request, type Response } from 'express'
|
|
52
52
|
import { timingSafeTokenEqual } from '../lib/token-auth.js'
|
|
53
|
-
import { turnEndAskStats } from '../lib/session-hooks-runtime.js'
|
|
53
|
+
import { turnEndAskStats, turnEndAsksForQueue } from '../lib/session-hooks-runtime.js'
|
|
54
|
+
import { buildQuestionQueue, type QuestionQueue } from '../lib/question-queue.js'
|
|
55
|
+
import { sessionTitleFor } from '../lib/session-title-cache.js'
|
|
56
|
+
import { waitingQuestionWordsEnabled } from '../lib/full-hold.js'
|
|
54
57
|
import {
|
|
55
58
|
ANSWERING_CLIENTS,
|
|
56
59
|
CLIENT_LIVE_WINDOW_MS,
|
|
@@ -163,6 +166,9 @@ export interface SessionQuestionsRouterDeps {
|
|
|
163
166
|
* rules: `isWebSessionAuthorized` in production). Absent: only the token opens the list, as before.
|
|
164
167
|
*/
|
|
165
168
|
webSessionAuthorized?: (req: Request) => boolean
|
|
169
|
+
/** X1 (the one queue): the turn-end asks and the session titles the queue reads. Default to the runtime's and the list's. */
|
|
170
|
+
turnEndQueue?: () => ReturnType<typeof turnEndAsksForQueue>
|
|
171
|
+
titleFor?: (sessionId: string) => string | null
|
|
166
172
|
/** 7.0.18 (X1): the turn-end ask counters (lib/turn-end-ask.ts), null while COS_TURN_END_ASKS is off. Defaults to the runtime's. */
|
|
167
173
|
turnEndAskStats?: () => Record<string, number> | null
|
|
168
174
|
/** Where a counted poll is recorded; `noteQuestionsPoll` in production. */
|
|
@@ -274,6 +280,26 @@ export function requestFromThisMac(
|
|
|
274
280
|
return own.some(a => normalizeRemoteIp(String(a)).toLowerCase() === ip)
|
|
275
281
|
}
|
|
276
282
|
|
|
283
|
+
/**
|
|
284
|
+
* X1 (the one queue, Miles 11/12 17:31): `queue` and `queueCount`, always present (whatever the full-hold switches),
|
|
285
|
+
* appended after every existing key so a client that reads the 7.0.31 keys reads exactly what it read before. A
|
|
286
|
+
* failure to build it never costs the rest of the response: the keys are then an empty queue.
|
|
287
|
+
*/
|
|
288
|
+
export function questionQueueFields(deps: SessionQuestionsRouterDeps): QuestionQueue {
|
|
289
|
+
try {
|
|
290
|
+
const turnEnd = (deps.turnEndQueue ?? turnEndAsksForQueue)()
|
|
291
|
+
return buildQuestionQueue({
|
|
292
|
+
broker: deps.broker.queueSources(),
|
|
293
|
+
turnEnd: turnEnd.asks,
|
|
294
|
+
turnEndAnswered: turnEnd.answered,
|
|
295
|
+
titleFor: deps.titleFor ?? sessionTitleFor,
|
|
296
|
+
words: waitingQuestionWordsEnabled(process.env),
|
|
297
|
+
})
|
|
298
|
+
} catch {
|
|
299
|
+
return { queue: [], queueCount: 0 }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
277
303
|
function turnEndAskCounters(deps: SessionQuestionsRouterDeps): { turnEndAsks?: Record<string, number> } {
|
|
278
304
|
let counters: Record<string, number> | null = null
|
|
279
305
|
try { counters = (deps.turnEndAskStats ?? turnEndAskStats)() } catch { counters = null }
|
|
@@ -341,6 +367,7 @@ export function createSessionQuestionsRouter(deps: SessionQuestionsRouterDeps):
|
|
|
341
367
|
// 7.0.18 (X1), additive and only while COS_TURN_END_ASKS is on: how many turn-end asks armed, were shown,
|
|
342
368
|
// were answered, were overtaken by a new turn, or aged out. Counts only, never words.
|
|
343
369
|
...turnEndAskCounters(deps),
|
|
370
|
+
...questionQueueFields(deps),
|
|
344
371
|
})
|
|
345
372
|
})
|
|
346
373
|
router.post('/session-questions/:id/answer', (req: Request, res: Response) => {
|
|
@@ -350,7 +377,9 @@ export function createSessionQuestionsRouter(deps: SessionQuestionsRouterDeps):
|
|
|
350
377
|
let expected = ''
|
|
351
378
|
try { expected = deps.apiToken() } catch { expected = '' }
|
|
352
379
|
const by = timingSafeTokenEqual(req.headers['x-cos-token'], expected) ? 'app' : 'web'
|
|
353
|
-
|
|
380
|
+
// X1 (the one queue): the answering client may name itself (glasses, phone, mac_app) so others can say where.
|
|
381
|
+
const client = req.get('x-cos-client') ?? (typeof req.query.client === 'string' ? req.query.client : undefined)
|
|
382
|
+
const result = deps.broker.answer(String(req.params.id ?? ''), req.body, by, client)
|
|
354
383
|
res.status(result.status).json(result.body)
|
|
355
384
|
})
|
|
356
385
|
// X1: the web door's confirm step for an approval (lib/permission-broker.ts `confirm`). The /api gate let the
|
|
@@ -12,6 +12,9 @@ import { refreshAfterInstall } from '../lib/provider-observe.js'
|
|
|
12
12
|
import { Router } from 'express'
|
|
13
13
|
import { installClaudeHooks, uninstallClaudeHooks } from '../lib/claude-hooks-installer.js'
|
|
14
14
|
import { deskIdleSeconds, invalidateHookStatus, sessionHooksHealthFields, sessionSignalStore } from '../lib/session-hooks-runtime.js'
|
|
15
|
+
import { API_AUTH_BY_LOCAL } from '../lib/api-auth.js'
|
|
16
|
+
import { questionHoldMode } from '../lib/full-hold.js'
|
|
17
|
+
import { writeQuestionHoldSetting } from '../lib/question-hold-setting.js'
|
|
15
18
|
import { workspaceFromCwd } from '../lib/claude-session-registry.js'
|
|
16
19
|
|
|
17
20
|
/** The registry entrypoints a person sits at; everything else (`sdk-cli`, unknown) may be a job. */
|
|
@@ -69,6 +72,26 @@ export function createSessionHooksRouter(options: { port: number; codex?: Sessio
|
|
|
69
72
|
res.json({ ok: true, ...sessionHooksHealthFields() })
|
|
70
73
|
})
|
|
71
74
|
|
|
75
|
+
// X1, 7.0.34: the question hold's server-side setting, for Control's Settings row (Control 0.5.394). Body
|
|
76
|
+
// `{"mode":"on"|"off"|"default"}`; `default` removes the setting. The Mac app only: a browser session is refused
|
|
77
|
+
// (Control Web shows the state read-only). `COS_QUESTION_FULL_HOLD` in the environment wins over the setting, so
|
|
78
|
+
// while it is set nothing is written and the answer says so (409 `set_by_env`). Turning the hold on or off here
|
|
79
|
+
// changes what Reinstall hooks writes; the status's `state` then says whether a Reinstall is due.
|
|
80
|
+
router.put('/session-hooks/question-hold', (req, res) => {
|
|
81
|
+
res.set('Cache-Control', 'private, no-store')
|
|
82
|
+
if (res.locals[API_AUTH_BY_LOCAL] !== 'token') { res.status(403).json({ ok: false, reason: 'web_read_only' }); return }
|
|
83
|
+
const mode = req.body && typeof req.body === 'object' ? (req.body as { mode?: unknown }).mode : undefined
|
|
84
|
+
if (mode !== 'on' && mode !== 'off' && mode !== 'default') { res.status(400).json({ ok: false, reason: 'bad_mode' }); return }
|
|
85
|
+
if (questionHoldMode(process.env).source === 'env') {
|
|
86
|
+
res.status(409).json({ ok: false, reason: 'set_by_env', questionWait: sessionHooksHealthFields().sessionHooks.questionWait ?? null })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
try { writeQuestionHoldSetting(mode) } catch { res.status(500).json({ ok: false, reason: 'write_failed' }); return }
|
|
90
|
+
invalidateHookStatus()
|
|
91
|
+
const hooks = sessionHooksHealthFields().sessionHooks
|
|
92
|
+
res.json({ ok: true, questionWait: hooks.questionWait ?? null, state: hooks.state })
|
|
93
|
+
})
|
|
94
|
+
|
|
72
95
|
router.post('/session-hooks/install', (req, res) => {
|
|
73
96
|
const dryRun = req.query.dryRun === '1' || (req.body && typeof req.body === 'object' && (req.body as { dryRun?: unknown }).dryRun === true)
|
|
74
97
|
const result = installClaudeHooks({ port: options.port, deskIdleSeconds: deskIdleSeconds(), dryRun })
|
|
@@ -309,6 +309,7 @@ sessionsRouter.get('/sessions/today/all-messages', async (req, res) => {
|
|
|
309
309
|
query: string
|
|
310
310
|
text: string
|
|
311
311
|
timestamp: number
|
|
312
|
+
queryTimestamp?: number
|
|
312
313
|
chatIndex: number
|
|
313
314
|
sessionId: string
|
|
314
315
|
source: 'live'
|
|
@@ -341,6 +342,8 @@ sessionsRouter.get('/sessions/today/all-messages', async (req, res) => {
|
|
|
341
342
|
query: ex.content,
|
|
342
343
|
text: next.content,
|
|
343
344
|
timestamp: next.timestamp,
|
|
345
|
+
// 7.0.33 A2: when the question was asked, beside when COS answered (timestamp), for a time under each.
|
|
346
|
+
...(Number.isFinite(ex.timestamp) ? { queryTimestamp: ex.timestamp } : {}),
|
|
344
347
|
chatIndex: -1,
|
|
345
348
|
sessionId: session.id,
|
|
346
349
|
source: 'live',
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"v": 1,
|
|
3
3
|
"algorithm": "sha256",
|
|
4
4
|
"files": {
|
|
5
|
-
"/app.css": "
|
|
6
|
-
"/app.js": "
|
|
5
|
+
"/app.css": "f2d9c657580959c8a49ce5dd947c027aded0f4e69e4367b8247f092c0ccb0c8f",
|
|
6
|
+
"/app.js": "c5740309a27ba9b1513be466e687ec483311045c2f0088ffb3d5507afb208db2",
|
|
7
7
|
"/apple-touch-icon.png": "04ec7eca0bf6f53d89900d146ac32d4826759d6258bcdbd1ccdcc517f26f532c",
|
|
8
|
-
"/control.css": "
|
|
8
|
+
"/control.css": "6943c7a0573ae85e1fbdbda36e1765ea4ee5d5fd9205690253570f89b49d7fd3",
|
|
9
9
|
"/fonts/DMSans.ttf": "8cd08d97e89c24d0aa92edd2f0f4c8ee6195eee9b7c9f154865a58b02f0c1c0d",
|
|
10
10
|
"/fonts/Fraunces.ttf": "177ff6c0f14e5550a3c624247cd1189611d4eb65d000b14944c63d967958abbb",
|
|
11
11
|
"/fonts/JetBrainsMono.ttf": "48715a42ec242c21e9f02692891e147d022299a52e48d5e413e1a942193ffeda",
|
|
12
|
-
"/index.html": "
|
|
12
|
+
"/index.html": "d9d1cede8b6ef2a3680f5b62e24fffcbf65e2eff3c33de31a69263f53d78520c",
|
|
13
13
|
"/mark.svg": "8e380d33d393027df192eb1afa08f15a380c2070c72d68f27311b49d9e94ca18",
|
|
14
14
|
"/memories/d3.min.js": "f2094bbf6141b359722c4fe454eb6c4b0f0e42cc10cc7af921fc158fceb86539",
|
|
15
15
|
"/memories/graph-explorer.css": "ec128de59811f45c71b6bededd737989309205e903951eb8211ade7664f87486",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"/panes/meetings.css": "635470e4d5f5665b046eab7382734c9772df569069c9b1eb10cfde6151014bfa",
|
|
23
23
|
"/panes/meetings.js": "e057f4dae697138111a4c07e8ec6c2a0502b5d93023cf491460bc7bf20172c85",
|
|
24
24
|
"/panes/memories.css": "36731d6fd21ad1516dfeccf34fe5b2d3d267d21e40b47704055a0685334b579e",
|
|
25
|
-
"/panes/memories.js": "
|
|
26
|
-
"/panes/messages.js": "
|
|
25
|
+
"/panes/memories.js": "47b9a06b329c3ec7c21902e6d8ec2fc7fe9013fffcfe936b54037be162c82958",
|
|
26
|
+
"/panes/messages.js": "5c43d171cb026ff98cc316821f631992288909570ec6abbd9bcc4dc1dc046bca",
|
|
27
27
|
"/panes/preview.js": "79f1aecb27f48d584ed4cf37c7f4f423f0f9b45dd6c2fb24a9d88c72625d8680",
|
|
28
28
|
"/panes/questions.css": "5c5a9b32ce32386b508ce326b17d40ecacbb0f2e049df7f4fe2a0009bd3b7908",
|
|
29
|
-
"/panes/questions.js": "
|
|
29
|
+
"/panes/questions.js": "60115b6f5d5380d6dac1ceb50e6a56bd6d10f8d49fb64429fbe73d6890270111",
|
|
30
30
|
"/panes/reader-media.js": "4cf892a855aa2d0dce6978e69296cd49b8036d8fc62f12773b3fcadc210b2ef0",
|
|
31
31
|
"/panes/sessions.css": "654e0509ab2b699b9ea92b6d9e2f7522146df1f0b6355915bef03caaf63addc4",
|
|
32
|
-
"/panes/settings.js": "
|
|
32
|
+
"/panes/settings.js": "cc2ec51f397383be0b6f5078e6632c35dab8bf12ed3555df20c7cf2ec8ae8f63",
|
|
33
33
|
"/panes/speakers.css": "49e70dae4266151b14b0057f2e43de169de225b8f3ac30343c81cf532735a487",
|
|
34
34
|
"/panes/speakers.js": "e4fbcc147c44f8bf798e0694e0c8b152d5cde82dc46823641c8e8d37714b8fba",
|
|
35
35
|
"/panes/threads.js": "45fd7b56480be53225e91193cfa4c41a4ee7167890f10560e439bc7d2461747f",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"/panes/today-actions.js": "c767b62b21a9f1debffd6e4ff4558891222b573535de1d78849b602030bb09bd",
|
|
38
38
|
"/panes/work.css": "da50355dac690f712d1f6331f70ee5eadbf5982d37508648ae7786afc99ac7b1",
|
|
39
39
|
"/panes/work.js": "913c56084a6df463cc83358ce360da30d529466f1c583683fd3a4fd5eafe2d30",
|
|
40
|
-
"/shell.js": "
|
|
40
|
+
"/shell.js": "9bef169c42d8130cbca8c012c3667a9b92cacaa8673b4cef47d6fc7cfc0096ef",
|
|
41
41
|
"/theme.js": "e3e6422eaac43979d03149da114f5b83757b7361dd7b309cdf76be36a596044b",
|
|
42
42
|
"/vendor/qrcode-encoder.js": "61d87cc7445cc66b862cff913de4571634746ea62bcd50d0f592be14f6d1ce46"
|
|
43
43
|
}
|
|
@@ -225,6 +225,26 @@ select.field {
|
|
|
225
225
|
.pet-caption { color: var(--sub); font-size: 14px; }
|
|
226
226
|
/* A still pet (gallery pick, Cloud Puff, single image). Small pixel art scales without blur. */
|
|
227
227
|
.pet-still { height: 140px; width: auto; max-width: 220px; object-fit: contain; }
|
|
228
|
+
/* 7.0.33 A13, Anand's mock (slide 9): in a phone's top bar, the pet in a small round button left of Ask, keeping its own
|
|
229
|
+
animation; a tap opens a card under it with the pet, its line, Change pet and Hide. The card floats, so it has the
|
|
230
|
+
mock's soft shadow and no border. */
|
|
231
|
+
.topbar .today-pet.in-bar { position: relative; min-height: 0; margin: 0 6px 0 auto; flex-direction: row; }
|
|
232
|
+
.today-pet.in-bar > .pet-btn { width: 36px; height: 36px; border-radius: 50%; justify-content: center; overflow: hidden;
|
|
233
|
+
background: color-mix(in srgb, var(--accent) 16%, var(--page)); border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent); }
|
|
234
|
+
.today-pet.in-bar > .pet-btn .pet-still { height: 30px; max-width: 30px; }
|
|
235
|
+
.today-pet.in-bar > .pet-btn .pet-sprite { zoom: 0.21; }
|
|
236
|
+
.today-pet.in-bar > .pet-btn .pet-caption, .today-pet.in-bar > .pet-change { display: none; }
|
|
237
|
+
.pet-card { position: absolute; top: calc(100% + 8px); right: -40px; width: min(250px, calc(100vw - 32px)); padding: 14px; text-align: left;
|
|
238
|
+
background: var(--panel); border-radius: 16px; box-shadow: 0 8px 28px rgba(40, 30, 15, 0.16); z-index: 30; }
|
|
239
|
+
.pet-card-head { display: flex; align-items: center; gap: 12px; width: 100%; background: none; border: 0; padding: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
240
|
+
.pet-card-head[aria-disabled="true"] { cursor: default; }
|
|
241
|
+
.pet-card-pet { flex: none; width: 44px; display: grid; place-items: center; }
|
|
242
|
+
.pet-card-pet .pet-still { height: 48px; max-width: 44px; }
|
|
243
|
+
.pet-card-pet .pet-sprite { zoom: 0.34; }
|
|
244
|
+
.pet-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
245
|
+
.pet-card-title { color: var(--ink); font-weight: 600; font-size: 16px; }
|
|
246
|
+
.pet-card-sub { color: var(--sub); font-size: 14px; line-height: 1.35; }
|
|
247
|
+
.pet-card-acts { display: flex; gap: 8px; margin-top: 12px; }
|
|
228
248
|
.pet-still.pixel { image-rendering: pixelated; }
|
|
229
249
|
.pet-change { background: none; border: 0; padding: 6px 10px; margin-top: 2px; color: var(--sub); font: inherit; font-size: 13px; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
|
230
250
|
.pet-change:focus-visible { outline: 2px solid var(--sub); outline-offset: 2px; border-radius: 6px; }
|
|
@@ -492,6 +512,9 @@ select.field {
|
|
|
492
512
|
.pane-head.with-ask { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; }
|
|
493
513
|
.pane-head.with-ask .ask-bar { max-width: 400px; margin-top: 0; }
|
|
494
514
|
.ask-bar .kbd { display: inline-block; }
|
|
515
|
+
/* 7.0.33 A9 (Anand round 2, slide 29): ⌘K is a keyboard's shortcut. A touch screen with no mouse (the open Duo, an
|
|
516
|
+
iPad held alone) does not show it. */
|
|
517
|
+
@media (hover: none) and (pointer: coarse) { .ask-bar .kbd { display: none; } }
|
|
495
518
|
.pane-title { font-size: 44px; }
|
|
496
519
|
.story { font-size: 26px; }
|
|
497
520
|
.run-row { grid-template-columns: 118px minmax(0, 1fr); gap: 2px 14px; align-items: start; }
|
|
@@ -704,8 +727,8 @@ html { touch-action: manipulation; }
|
|
|
704
727
|
/* The focus ring sits inside the button: the rail clips anything past its edge. */
|
|
705
728
|
html.short-screen .rail :focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; border-radius: 12px; }
|
|
706
729
|
/* One way to switch panes on a screen: the rail. On a list the top bar keeps the pane's name, without the caret and
|
|
707
|
-
its menu (app.js makes the button inert)
|
|
708
|
-
|
|
709
|
-
html.short-screen .section-btn .caret
|
|
730
|
+
its menu (app.js makes the button inert). On a reader (7.0.33 A12) the top bar shows Back with the name of where it
|
|
731
|
+
goes, and stays put while you read; the rail's foot keeps its own Back within the thumb's reach. */
|
|
732
|
+
html.short-screen .section-btn .caret { display: none !important; }
|
|
710
733
|
html.short-screen .section-btn { cursor: default; }
|
|
711
734
|
}
|