@gotcos/glasses-server 6.44.2 → 6.44.3
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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/server/lib/task-store.ts +28 -0
- package/server/routes/tasks.ts +47 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 6.44.3
|
|
2
|
+
|
|
3
|
+
A task row carries enough to show and edit it.
|
|
4
|
+
|
|
5
|
+
- `GET /api/tasks` rows gain `text` (the whole description), `source`, `checked`, and `owner`/`agentState`/`agentNo` when present. `title` still exists and is still capped at 44 characters, which is a G2 lens row budget: applying it to every surface is why COS Control rendered "Send James/John the call recording + recap…" in a window 1900px wide. Additive on purpose so clients through 6.9.451, which read `title`, keep working.
|
|
6
|
+
- `PUT /api/domains` sets the user's domain list, so a settings pane can write it instead of the user hand-editing `~/.cos-glasses/.cos-profile.json`. It replaces the configured list wholesale and validates each name for path safety; discovered folders are never written, so removing a name from config cannot hide a folder that still holds tasks.
|
|
7
|
+
- `PATCH /api/tasks/:id` accepts `text`, rewriting a task's words through a new `task-set-text` bridge subcommand. The source block, the `[run ...]` schedule, and any agent marker are rebuilt from parsed state, so editing the words never drops provenance. A task with a running agent is refused, since rewriting the line would change the task the agent was dispatched against.
|
|
8
|
+
|
|
1
9
|
## 6.44.2
|
|
2
10
|
|
|
3
11
|
Task domains come from the user, not from the build.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.44.
|
|
3
|
+
"version": "6.44.3",
|
|
4
4
|
"description": "COS Glasses \u2014 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": {
|
package/server/lib/task-store.ts
CHANGED
|
@@ -116,7 +116,16 @@ export interface TaskBoardRow {
|
|
|
116
116
|
id: string
|
|
117
117
|
ref: string
|
|
118
118
|
domain: string
|
|
119
|
+
/** Capped to TASK_TITLE_MAX for a lens row. */
|
|
119
120
|
title: string
|
|
121
|
+
/** The whole description, for surfaces with room. */
|
|
122
|
+
text: string
|
|
123
|
+
source?: string
|
|
124
|
+
owner?: string
|
|
125
|
+
delegated?: boolean
|
|
126
|
+
agentState?: 'running' | 'done' | 'failed'
|
|
127
|
+
agentNo?: number
|
|
128
|
+
checked: boolean
|
|
120
129
|
column: TaskColumn
|
|
121
130
|
priority: string
|
|
122
131
|
runAt?: string
|
|
@@ -389,6 +398,17 @@ export function projectRow(
|
|
|
389
398
|
ref: row.ref,
|
|
390
399
|
domain: row.domain,
|
|
391
400
|
title: taskTitle(row.description),
|
|
401
|
+
// `title` stays capped for the lens; `text` is the whole line. The cap is a
|
|
402
|
+
// G2 row budget and was being applied to every surface, so a Mac window
|
|
403
|
+
// 1900px wide showed "Send James/John the call recording + recap…". Additive
|
|
404
|
+
// on purpose: clients through 6.9.451 read `title` and must keep working.
|
|
405
|
+
text: row.description,
|
|
406
|
+
...(row.source ? { source: row.source } : {}),
|
|
407
|
+
...(row.owner ? { owner: row.owner } : {}),
|
|
408
|
+
...(row.delegated ? { delegated: true } : {}),
|
|
409
|
+
...(row.agent_state ? { agentState: row.agent_state } : {}),
|
|
410
|
+
...(row.agent_no != null ? { agentNo: row.agent_no } : {}),
|
|
411
|
+
checked: row.is_checked,
|
|
392
412
|
column: col,
|
|
393
413
|
priority: row.priority,
|
|
394
414
|
...(runAt ? { runAt: new Date(taskInstant(runAt.day, runAt.minutes, tz)).toISOString() } : {}),
|
|
@@ -458,6 +478,14 @@ export async function setTaskRunAt(domain: string, id: string, runAt: string | n
|
|
|
458
478
|
await withLockRetry(() => bridge(args))
|
|
459
479
|
}
|
|
460
480
|
|
|
481
|
+
/** Rewrite a task's words. The bridge preserves its source block and schedule. */
|
|
482
|
+
export async function setTaskText(domain: string, id: string, text: string): Promise<void> {
|
|
483
|
+
const clean = text.replace(/\s+/g, ' ').trim()
|
|
484
|
+
if (!clean) throw new TaskRunError(400, 'text_required', 'text is required')
|
|
485
|
+
if (clean.length > 2000) throw new TaskRunError(422, 'text_too_long', 'text must be 2000 characters or fewer')
|
|
486
|
+
await withLockRetry(() => bridge(['task-set-text', domain, id], clean))
|
|
487
|
+
}
|
|
488
|
+
|
|
461
489
|
export async function moveTask(domain: string, id: string, section: string, nowMs = Date.now()): Promise<void> {
|
|
462
490
|
const { clock } = briefContext(nowMs)
|
|
463
491
|
const args = ['task-move', domain, id, '--section', section]
|
package/server/routes/tasks.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Router } from 'express'
|
|
2
2
|
import { queryJobCoordinator } from '../lib/query-job-runtime.js'
|
|
3
3
|
import { runTaskNow } from '../lib/task-dispatcher.js'
|
|
4
|
-
import { domainAbbreviation } from '../lib/domains.js'
|
|
4
|
+
import { domainAbbreviation, isSafeDomainName } from '../lib/domains.js'
|
|
5
|
+
import { updateProfileFields } from '../lib/profile.js'
|
|
5
6
|
import { domainLabel } from '../lib/domain-label.js'
|
|
6
7
|
import {
|
|
7
8
|
TaskBridgeError,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
moveTask,
|
|
15
16
|
saveDispatchCap,
|
|
16
17
|
setTaskRunAt,
|
|
18
|
+
setTaskText,
|
|
17
19
|
taskDomains,
|
|
18
20
|
tasksGate,
|
|
19
21
|
workBadgeCount,
|
|
@@ -127,6 +129,46 @@ tasksRouter.get('/domains', (_req, res) => {
|
|
|
127
129
|
}
|
|
128
130
|
})
|
|
129
131
|
|
|
132
|
+
/** Set the user's domain list. Replaces it wholesale: the client shows the
|
|
133
|
+
* current list and sends back what it wants, which is what a settings pane does.
|
|
134
|
+
* Discovered folders are NOT written here — they keep showing because the read
|
|
135
|
+
* side unions them in, so removing a name from config never hides a folder that
|
|
136
|
+
* still holds tasks. */
|
|
137
|
+
tasksRouter.put('/domains', (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const raw = (req.body ?? {}).domains
|
|
140
|
+
if (!Array.isArray(raw)) throw new TaskRunError(400, 'domains_required', 'domains must be an array')
|
|
141
|
+
if (raw.length > 32) throw new TaskRunError(422, 'too_many_domains', 'at most 32 domains')
|
|
142
|
+
const names: string[] = []
|
|
143
|
+
const seen = new Set<string>()
|
|
144
|
+
for (const entry of raw) {
|
|
145
|
+
const name = typeof entry === 'string'
|
|
146
|
+
? entry
|
|
147
|
+
: (entry && typeof entry === 'object' && typeof (entry as { name?: unknown }).name === 'string')
|
|
148
|
+
? (entry as { name: string }).name
|
|
149
|
+
: ''
|
|
150
|
+
const trimmed = name.trim()
|
|
151
|
+
if (!trimmed) continue
|
|
152
|
+
if (!isSafeDomainName(trimmed)) {
|
|
153
|
+
throw new TaskRunError(422, 'invalid_domain_name', `not a usable domain name: ${trimmed}`)
|
|
154
|
+
}
|
|
155
|
+
const key = trimmed.toLowerCase()
|
|
156
|
+
if (seen.has(key)) continue
|
|
157
|
+
seen.add(key)
|
|
158
|
+
names.push(trimmed)
|
|
159
|
+
}
|
|
160
|
+
updateProfileFields({ domains: names })
|
|
161
|
+
const resolved = taskDomains()
|
|
162
|
+
res.json({
|
|
163
|
+
domains: resolved.map(name => ({ name, abbr: domainAbbreviation(name), label: domainLabel(name) })),
|
|
164
|
+
configured: names,
|
|
165
|
+
gate: tasksGate(),
|
|
166
|
+
})
|
|
167
|
+
} catch (error) {
|
|
168
|
+
sendError(res, error)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
130
172
|
tasksRouter.get('/tasks', async (req, res) => {
|
|
131
173
|
try {
|
|
132
174
|
const column = typeof req.query.column === 'string' ? req.query.column : undefined
|
|
@@ -141,14 +183,16 @@ tasksRouter.patch('/tasks/:id', async (req, res) => {
|
|
|
141
183
|
try {
|
|
142
184
|
const body = req.body ?? {}
|
|
143
185
|
const domain = requireDomain(body.domain)
|
|
144
|
-
if ('
|
|
186
|
+
if ('text' in body) {
|
|
187
|
+
await setTaskText(domain, req.params.id, String(body.text))
|
|
188
|
+
} else if ('runAt' in body) {
|
|
145
189
|
await setTaskRunAt(domain, req.params.id, body.runAt == null || body.runAt === '' ? null : String(body.runAt))
|
|
146
190
|
} else if ('section' in body) {
|
|
147
191
|
await moveTask(domain, req.params.id, String(body.section))
|
|
148
192
|
} else if ('checked' in body) {
|
|
149
193
|
await checkTask({ domain, id: req.params.id, checked: body.checked === true })
|
|
150
194
|
} else {
|
|
151
|
-
throw new TaskRunError(400, 'invalid_patch', 'Expected runAt, section, or checked.')
|
|
195
|
+
throw new TaskRunError(400, 'invalid_patch', 'Expected text, runAt, section, or checked.')
|
|
152
196
|
}
|
|
153
197
|
res.json({ ok: true })
|
|
154
198
|
} catch (error) {
|