thinkpool-pair 0.7.364 → 0.7.366
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/LICENSE +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
|
@@ -1,53 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// facts. Deliver the first immediately and coalesce a burst to its latest frame;
|
|
3
|
-
// any durable event flushes the pending snapshot first to preserve wire order.
|
|
4
|
-
//
|
|
5
|
-
// 75ms is a readable 13.3fps ceiling: quick enough for an active reply without
|
|
6
|
-
// turning every provider delta into a realtime broadcast or a full UI repaint.
|
|
7
|
-
export const CUMULATIVE_EVENT_RELAY_WAIT_MS = 75
|
|
8
|
-
|
|
9
|
-
export function createCumulativeEventRelay(emit, waitMs = CUMULATIVE_EVENT_RELAY_WAIT_MS) {
|
|
10
|
-
let lastSentAt = 0
|
|
11
|
-
let pending = null
|
|
12
|
-
let timer = null
|
|
13
|
-
|
|
14
|
-
const send = (event) => {
|
|
15
|
-
lastSentAt = Date.now()
|
|
16
|
-
emit(event)
|
|
17
|
-
}
|
|
18
|
-
const flush = () => {
|
|
19
|
-
if (timer) clearTimeout(timer)
|
|
20
|
-
timer = null
|
|
21
|
-
if (!pending) return
|
|
22
|
-
const event = pending
|
|
23
|
-
pending = null
|
|
24
|
-
send(event)
|
|
25
|
-
}
|
|
26
|
-
const relay = (event) => {
|
|
27
|
-
if (event?.kind !== 'assistant_stream') {
|
|
28
|
-
flush()
|
|
29
|
-
emit(event)
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
const remaining = waitMs - (Date.now() - lastSentAt)
|
|
33
|
-
if (!lastSentAt || remaining <= 0) {
|
|
34
|
-
pending = null
|
|
35
|
-
if (timer) clearTimeout(timer)
|
|
36
|
-
timer = null
|
|
37
|
-
send(event)
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
pending = event
|
|
41
|
-
if (!timer) {
|
|
42
|
-
timer = setTimeout(flush, remaining)
|
|
43
|
-
timer.unref?.()
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
relay.flush = flush
|
|
47
|
-
relay.cancel = () => {
|
|
48
|
-
if (timer) clearTimeout(timer)
|
|
49
|
-
timer = null
|
|
50
|
-
pending = null
|
|
51
|
-
}
|
|
52
|
-
return relay
|
|
53
|
-
}
|
|
1
|
+
export const CUMULATIVE_EVENT_RELAY_WAIT_MS=75;export function createCumulativeEventRelay(e,n=75){let t=0,l=null,u=null;const r=n=>{t=Date.now(),e(n)},o=()=>{if(u&&clearTimeout(u),u=null,!l)return;const e=l;l=null,r(e)},i=i=>{if("assistant_stream"!==i?.kind)return o(),void e(i);const a=n-(Date.now()-t);if(!t||a<=0)return l=null,u&&clearTimeout(u),u=null,void r(i);l=i,u||(u=setTimeout(o,a),u.unref?.())};return i.flush=o,i.cancel=()=>{u&&clearTimeout(u),u=null,l=null},i}
|
package/design-edit.mjs
CHANGED
|
@@ -1,424 +1 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import {
|
|
6
|
-
DESIGN_SOURCE_MAP_RELATIVE,
|
|
7
|
-
DESIGN_SOURCE_MAP_VERSION,
|
|
8
|
-
MAX_DESIGN_SOURCE_ENTRIES,
|
|
9
|
-
MAX_DESIGN_SOURCE_MAP_BYTES,
|
|
10
|
-
} from './design-source-contract.mjs'
|
|
11
|
-
|
|
12
|
-
const MAX_SOURCE_BYTES = 2 * 1024 * 1024
|
|
13
|
-
const MAX_INTENT = 2_000
|
|
14
|
-
const MAX_TEXT = 4_000
|
|
15
|
-
const MAX_BATCH_EDITS = 8
|
|
16
|
-
const GENERATED_SOURCE_PARTS = new Set(['dist', 'build', '.next', '.git', 'coverage', 'node_modules'])
|
|
17
|
-
|
|
18
|
-
const clean = (value, max) => String(value ?? '').replace(/\0/g, '').trim().slice(0, max)
|
|
19
|
-
const isInside = (parent, child) => {
|
|
20
|
-
const rel = path.relative(parent, child)
|
|
21
|
-
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// supabase-js does not copy the Auth session into an already-created Realtime
|
|
25
|
-
// client. Private tpdesign:* joins therefore need the bridge owner's JWT pinned
|
|
26
|
-
// explicitly, both at boot and whenever the account supervisor rotates it.
|
|
27
|
-
export async function syncRealtimeAuth(client, accessToken, fallbackToken = null) {
|
|
28
|
-
const token = accessToken || fallbackToken
|
|
29
|
-
if (!token || typeof client?.realtime?.setAuth !== 'function') return false
|
|
30
|
-
try {
|
|
31
|
-
await client.realtime.setAuth(token)
|
|
32
|
-
return true
|
|
33
|
-
} catch {
|
|
34
|
-
return false
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function sourceRevision(source) {
|
|
39
|
-
return crypto.createHash('sha256').update(String(source)).digest('hex').slice(0, 24)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function resolveDesignSourceMap(file, previewRoot, workspaceRoot) {
|
|
43
|
-
if (!file || !previewRoot || !workspaceRoot) return null
|
|
44
|
-
let sourceMapPath, expectedSourceMapPath, builtRoot, root, raw, parsed
|
|
45
|
-
try {
|
|
46
|
-
sourceMapPath = fs.realpathSync(file)
|
|
47
|
-
builtRoot = fs.realpathSync(previewRoot)
|
|
48
|
-
expectedSourceMapPath = fs.realpathSync(path.join(builtRoot, DESIGN_SOURCE_MAP_RELATIVE))
|
|
49
|
-
root = fs.realpathSync(workspaceRoot)
|
|
50
|
-
const stat = fs.statSync(sourceMapPath)
|
|
51
|
-
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_DESIGN_SOURCE_MAP_BYTES) return null
|
|
52
|
-
if (sourceMapPath !== expectedSourceMapPath || !isInside(root, builtRoot) || !isInside(builtRoot, sourceMapPath)) return null
|
|
53
|
-
raw = fs.readFileSync(sourceMapPath, 'utf8')
|
|
54
|
-
parsed = JSON.parse(raw)
|
|
55
|
-
} catch { return null }
|
|
56
|
-
if (parsed?.version !== DESIGN_SOURCE_MAP_VERSION || !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) return null
|
|
57
|
-
const rows = Object.entries(parsed.entries)
|
|
58
|
-
if (rows.length > MAX_DESIGN_SOURCE_ENTRIES) return null
|
|
59
|
-
|
|
60
|
-
const files = new Map()
|
|
61
|
-
const entries = new Map()
|
|
62
|
-
for (const [sourceId, value] of rows) {
|
|
63
|
-
if (!/^[A-Za-z0-9_-]{8,80}$/.test(sourceId) || !value || typeof value !== 'object') return null
|
|
64
|
-
const relative = clean(value.file, 700)
|
|
65
|
-
if (!relative || path.isAbsolute(relative) || relative.split(/[\\/]/).some((part) => part === '..' || GENERATED_SOURCE_PARTS.has(part))) return null
|
|
66
|
-
let sourcePath = files.get(relative)
|
|
67
|
-
if (!sourcePath) {
|
|
68
|
-
try {
|
|
69
|
-
sourcePath = fs.realpathSync(path.resolve(root, relative))
|
|
70
|
-
const stat = fs.statSync(sourcePath)
|
|
71
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES || !isInside(root, sourcePath)) return null
|
|
72
|
-
} catch { return null }
|
|
73
|
-
files.set(relative, sourcePath)
|
|
74
|
-
}
|
|
75
|
-
const start = Number(value.start)
|
|
76
|
-
const end = Number(value.end)
|
|
77
|
-
const line = Number(value.line)
|
|
78
|
-
const column = Number(value.column)
|
|
79
|
-
const tag = clean(value.tag, 40).toLowerCase()
|
|
80
|
-
const fileHash = clean(value.fileHash, 64).toLowerCase()
|
|
81
|
-
if (![start, end, line, column].every(Number.isSafeInteger) || start < 0 || end <= start || line < 1 || column < 0) return null
|
|
82
|
-
if (!/^[a-z][a-z0-9:-]*$/.test(tag) || !/^[a-f0-9]{24}$/.test(fileHash)) return null
|
|
83
|
-
entries.set(sourceId, { sourceId, file: relative.split(path.sep).join('/'), sourcePath, start, end, line, column, tag, fileHash })
|
|
84
|
-
}
|
|
85
|
-
return { path: sourceMapPath, revision: sourceRevision(raw), buildId: clean(parsed.buildId, 80), entries }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function resolveDesignSource(file, workspaceRoot) {
|
|
89
|
-
if (!file || !workspaceRoot) return null
|
|
90
|
-
let sourcePath, root
|
|
91
|
-
try {
|
|
92
|
-
sourcePath = fs.realpathSync(file)
|
|
93
|
-
root = fs.realpathSync(workspaceRoot)
|
|
94
|
-
const stat = fs.statSync(sourcePath)
|
|
95
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null
|
|
96
|
-
} catch { return null }
|
|
97
|
-
const rel = path.relative(root, sourcePath)
|
|
98
|
-
if (!rel || rel.startsWith('..') || path.isAbsolute(rel) || path.extname(sourcePath).toLowerCase() !== '.html') return null
|
|
99
|
-
// Generated bundles are evidence, not editable source. The agent may only receive
|
|
100
|
-
// an exact authored HTML artifact; it must never be pointed at dist output.
|
|
101
|
-
if (rel.split(path.sep).some((part) => ['dist', 'build', '.next', '.git', 'coverage', 'node_modules'].includes(part))) return null
|
|
102
|
-
const source = fs.readFileSync(sourcePath, 'utf8')
|
|
103
|
-
const revision = sourceRevision(source)
|
|
104
|
-
const previewId = crypto.createHash('sha256')
|
|
105
|
-
.update(`${sourcePath}\0${revision}`)
|
|
106
|
-
.digest('base64url').slice(0, 32)
|
|
107
|
-
return { previewId, revision, sourcePath, workspaceRoot: root, source }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function resolvePreviewDesignSource(manifest, workspaceRoot, box) {
|
|
111
|
-
if (!manifest || manifest.sourceKind !== 'preview' || !manifest.snapshot || !workspaceRoot || !box) return null
|
|
112
|
-
let sourcePath, root, outbox
|
|
113
|
-
try {
|
|
114
|
-
sourcePath = fs.realpathSync(manifest.snapshot)
|
|
115
|
-
root = fs.realpathSync(workspaceRoot)
|
|
116
|
-
outbox = fs.realpathSync(box)
|
|
117
|
-
const stat = fs.statSync(sourcePath)
|
|
118
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null
|
|
119
|
-
} catch { return null }
|
|
120
|
-
const rel = path.relative(outbox, sourcePath)
|
|
121
|
-
if (!rel || rel.startsWith('..') || path.isAbsolute(rel) || path.extname(sourcePath).toLowerCase() !== '.html') return null
|
|
122
|
-
const route = clean(manifest.route || '/', 500)
|
|
123
|
-
const previewRoot = clean(manifest.previewRoot || 'dist', 500)
|
|
124
|
-
const captureKey = clean(manifest.captureKey, 1000)
|
|
125
|
-
if (!route.startsWith('/') || !captureKey || captureKey !== JSON.stringify([previewRoot, route])) return null
|
|
126
|
-
const source = fs.readFileSync(sourcePath, 'utf8')
|
|
127
|
-
let builtRoot = null
|
|
128
|
-
try {
|
|
129
|
-
builtRoot = fs.realpathSync(path.resolve(root, previewRoot))
|
|
130
|
-
if (!isInside(root, builtRoot)) return null
|
|
131
|
-
} catch { return null }
|
|
132
|
-
const sourceMap = manifest.sourceMap ? resolveDesignSourceMap(manifest.sourceMap, builtRoot, root) : null
|
|
133
|
-
const revision = sourceRevision(sourceMap ? `${source}\0${sourceMap.revision}` : source)
|
|
134
|
-
const previewId = crypto.createHash('sha256')
|
|
135
|
-
.update(`${root}\0${captureKey}\0${revision}`)
|
|
136
|
-
.digest('base64url').slice(0, 32)
|
|
137
|
-
return {
|
|
138
|
-
sourceKind: 'preview', previewId, revision, sourcePath, workspaceRoot: root, source, outbox,
|
|
139
|
-
route, previewRoot, captureKey, sourceMap, sourceMapPath: sourceMap?.path || null,
|
|
140
|
-
sourceMapDeclared: !!manifest.sourceMap,
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Older lane worktrees predate the explicit `source` manifest field and emit the
|
|
145
|
-
// authored mockup path as `html` only. Preserve the explicit field when present;
|
|
146
|
-
// otherwise let the same containment/generated-output checks decide whether the
|
|
147
|
-
// HTML path is canonical editable source or view-only render evidence.
|
|
148
|
-
export function resolveManifestDesignSource(manifest, workspaceRoot, { box } = {}) {
|
|
149
|
-
if (!manifest || typeof manifest !== 'object') return null
|
|
150
|
-
if (manifest.sourceKind === 'preview') return resolvePreviewDesignSource(manifest, workspaceRoot, box)
|
|
151
|
-
return resolveDesignSource(manifest.source || manifest.html, workspaceRoot)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Authored mockups keep two representations: the canonical workspace source the
|
|
155
|
-
// producing lane edits, and a settled inert DOM snapshot the Design viewer shows.
|
|
156
|
-
// Snapshot paths are accepted only from this lane's private outbox so a crafted
|
|
157
|
-
// manifest cannot upload an arbitrary host file as room-visible HTML.
|
|
158
|
-
export function resolveManifestDisplaySource(manifest, record, { box } = {}) {
|
|
159
|
-
if (!record) return null
|
|
160
|
-
if (record.sourceKind === 'preview' || !manifest?.snapshot) return record.source
|
|
161
|
-
let snapshotPath, outbox
|
|
162
|
-
try {
|
|
163
|
-
snapshotPath = fs.realpathSync(manifest.snapshot)
|
|
164
|
-
outbox = fs.realpathSync(box)
|
|
165
|
-
const stat = fs.statSync(snapshotPath)
|
|
166
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null
|
|
167
|
-
} catch { return null }
|
|
168
|
-
const rel = path.relative(outbox, snapshotPath)
|
|
169
|
-
if (!rel || rel.startsWith('..') || path.isAbsolute(rel) || path.extname(snapshotPath).toLowerCase() !== '.html') return null
|
|
170
|
-
return fs.readFileSync(snapshotPath, 'utf8')
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function refreshDesignSource(record) {
|
|
174
|
-
if (!record) return null
|
|
175
|
-
if (record.sourceKind !== 'preview') return resolveDesignSource(record.sourcePath, record.workspaceRoot)
|
|
176
|
-
return resolvePreviewDesignSource({
|
|
177
|
-
sourceKind: 'preview', snapshot: record.sourcePath, route: record.route,
|
|
178
|
-
previewRoot: record.previewRoot, captureKey: record.captureKey, sourceMap: record.sourceMapPath,
|
|
179
|
-
}, record.workspaceRoot, record.outbox)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Design provenance is intentionally host-only, but it must survive a bridge
|
|
183
|
-
// restart. Rebuild the in-memory registry from manifests already written by the
|
|
184
|
-
// trusted lane render workflow; do not re-upload or re-broadcast old cards.
|
|
185
|
-
export function restoreDesignSources(box, workspaceRoot, term, limit = 100) {
|
|
186
|
-
if (!box || !workspaceRoot || !term) return []
|
|
187
|
-
let files
|
|
188
|
-
try {
|
|
189
|
-
files = fs.readdirSync(box, { withFileTypes: true })
|
|
190
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.json') && !entry.name.endsWith('.receipt.json'))
|
|
191
|
-
.map((entry) => {
|
|
192
|
-
const file = path.join(box, entry.name)
|
|
193
|
-
return { file, mtimeMs: fs.statSync(file).mtimeMs }
|
|
194
|
-
})
|
|
195
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
196
|
-
.slice(0, Math.max(0, limit))
|
|
197
|
-
} catch { return [] }
|
|
198
|
-
const restored = []
|
|
199
|
-
for (const { file } of files) {
|
|
200
|
-
try {
|
|
201
|
-
const manifest = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
202
|
-
const record = resolveManifestDesignSource(manifest, workspaceRoot, { box })
|
|
203
|
-
if (!record) continue
|
|
204
|
-
Object.assign(record, {
|
|
205
|
-
term,
|
|
206
|
-
slug: String(manifest.slug || ''),
|
|
207
|
-
title: String(manifest.title || manifest.slug || ''),
|
|
208
|
-
desktop: manifest.desktop || null,
|
|
209
|
-
mobile: manifest.mobile || null,
|
|
210
|
-
})
|
|
211
|
-
const backupPath = path.join(box, '.design-revisions', `${record.revision}.html`)
|
|
212
|
-
if (fs.existsSync(backupPath)) record.backupPath = backupPath
|
|
213
|
-
restored.push(record)
|
|
214
|
-
} catch { /* stale or malformed manifests are ignored */ }
|
|
215
|
-
}
|
|
216
|
-
return restored
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function safeTarget(value) {
|
|
220
|
-
if (!value || typeof value !== 'object') return null
|
|
221
|
-
const rect = value.rect && typeof value.rect === 'object' ? {
|
|
222
|
-
x: Number(value.rect.x) || 0,
|
|
223
|
-
y: Number(value.rect.y) || 0,
|
|
224
|
-
width: Math.max(0, Number(value.rect.width) || 0),
|
|
225
|
-
height: Math.max(0, Number(value.rect.height) || 0),
|
|
226
|
-
} : null
|
|
227
|
-
return {
|
|
228
|
-
selector: clean(value.selector, 600),
|
|
229
|
-
xpath: clean(value.xpath, 600),
|
|
230
|
-
tag: clean(value.tag, 40).toLowerCase(),
|
|
231
|
-
role: clean(value.role, 80),
|
|
232
|
-
name: clean(value.name, 240),
|
|
233
|
-
text: clean(value.text, MAX_TEXT),
|
|
234
|
-
canEditText: value.canEditText === true,
|
|
235
|
-
canReplaceImage: value.canReplaceImage === true,
|
|
236
|
-
nodeKey: clean(value.nodeKey, 80),
|
|
237
|
-
parentNodeKey: clean(value.parentNodeKey, 80),
|
|
238
|
-
sourceId: clean(value.sourceId, 80),
|
|
239
|
-
parentSourceId: clean(value.parentSourceId, 80),
|
|
240
|
-
rect,
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function resolveRequestSourceIdentity(record, target) {
|
|
245
|
-
if (!target?.sourceId) return { ok: true, identity: null }
|
|
246
|
-
const entry = record?.sourceMap?.entries?.get(target.sourceId)
|
|
247
|
-
if (!entry) return { ok: false, error: record?.sourceMapDeclared ? 'stale-source-identity' : 'source-identity-unavailable' }
|
|
248
|
-
if (target.tag && entry.tag !== target.tag) return { ok: false, error: 'source-identity-mismatch' }
|
|
249
|
-
let source
|
|
250
|
-
try { source = fs.readFileSync(entry.sourcePath, 'utf8') } catch { return { ok: false, error: 'stale-source-identity' } }
|
|
251
|
-
if (sourceRevision(source) !== entry.fileHash) return { ok: false, error: 'stale-source-identity' }
|
|
252
|
-
return {
|
|
253
|
-
ok: true,
|
|
254
|
-
identity: {
|
|
255
|
-
sourceId: entry.sourceId,
|
|
256
|
-
file: entry.file,
|
|
257
|
-
start: entry.start,
|
|
258
|
-
end: entry.end,
|
|
259
|
-
line: entry.line,
|
|
260
|
-
column: entry.column,
|
|
261
|
-
tag: entry.tag,
|
|
262
|
-
fileHash: entry.fileHash,
|
|
263
|
-
},
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function safeMovement(value) {
|
|
268
|
-
if (!value || typeof value !== 'object') return null
|
|
269
|
-
const fromIndex = Number(value.fromIndex)
|
|
270
|
-
const toIndex = Number(value.toIndex)
|
|
271
|
-
if (![fromIndex, toIndex].every(Number.isSafeInteger) || fromIndex < 0 || toIndex < 0 || fromIndex > 499 || toIndex > 499) return null
|
|
272
|
-
return { fromIndex, toIndex }
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function safeAsset(value, room) {
|
|
276
|
-
if (!value || typeof value !== 'object') return null
|
|
277
|
-
const asset = {
|
|
278
|
-
path: clean(value.path, 700),
|
|
279
|
-
name: clean(value.name, 240),
|
|
280
|
-
mediaType: clean(value.mediaType, 120).toLowerCase(),
|
|
281
|
-
size: Number(value.size),
|
|
282
|
-
}
|
|
283
|
-
const prefix = room ? `code/${String(room).toUpperCase()}/design-drafts/` : 'code/'
|
|
284
|
-
if (!asset.path.startsWith(prefix) || asset.path.split('/').includes('..') || !asset.mediaType.startsWith('image/')) return null
|
|
285
|
-
if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > 10 * 1024 * 1024) return null
|
|
286
|
-
return asset
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function validateDesignRequest(payload, registry, room = '') {
|
|
290
|
-
if (!payload || typeof payload !== 'object') return { ok: false, error: 'invalid-request' }
|
|
291
|
-
const previewId = clean(payload.previewId, 64)
|
|
292
|
-
const record = registry.get(previewId)
|
|
293
|
-
if (!record) return { ok: false, error: 'preview-unavailable' }
|
|
294
|
-
if (clean(payload.revision, 64) !== record.revision) return { ok: false, error: 'stale-preview' }
|
|
295
|
-
const target = safeTarget(payload.target)
|
|
296
|
-
if (!target?.selector && !target?.xpath) return { ok: false, error: 'select-an-element' }
|
|
297
|
-
const mode = ['text', 'prompt', 'move', 'image'].includes(payload.mode) ? payload.mode : null
|
|
298
|
-
if (!mode) return { ok: false, error: 'invalid-mode' }
|
|
299
|
-
const intent = clean(payload.intent, mode === 'text' ? MAX_TEXT : MAX_INTENT)
|
|
300
|
-
if (['text', 'prompt'].includes(mode) && !intent) return { ok: false, error: mode === 'text' ? 'enter-text' : 'describe-change' }
|
|
301
|
-
if (mode === 'text' && !target.canEditText) return { ok: false, error: 'text-edit-not-safe' }
|
|
302
|
-
const movement = mode === 'move' ? safeMovement(payload.movement) : null
|
|
303
|
-
if (mode === 'move' && (!movement || movement.fromIndex === movement.toIndex)) return { ok: false, error: 'invalid-move' }
|
|
304
|
-
const asset = mode === 'image' ? safeAsset(payload.asset, room) : null
|
|
305
|
-
if (mode === 'image' && (!target.canReplaceImage || !asset)) return { ok: false, error: 'invalid-image' }
|
|
306
|
-
const source = resolveRequestSourceIdentity(record, target)
|
|
307
|
-
if (!source.ok) return source
|
|
308
|
-
return {
|
|
309
|
-
ok: true,
|
|
310
|
-
record,
|
|
311
|
-
request: {
|
|
312
|
-
previewId, revision: record.revision, mode, intent, target, movement, asset,
|
|
313
|
-
sourceIdentity: source.identity, cid: clean(payload.cid, 80),
|
|
314
|
-
},
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export function validateDesignBatchRequest(payload, registry, room = '') {
|
|
319
|
-
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.edits)) return validateDesignRequest(payload, registry, room)
|
|
320
|
-
if (payload.edits.length < 1 || payload.edits.length > MAX_BATCH_EDITS) return { ok: false, error: 'invalid-batch' }
|
|
321
|
-
const previewId = clean(payload.previewId, 64)
|
|
322
|
-
const revision = clean(payload.revision, 64)
|
|
323
|
-
const cid = clean(payload.cid, 80)
|
|
324
|
-
const edits = []
|
|
325
|
-
const targets = new Set()
|
|
326
|
-
let record = null
|
|
327
|
-
for (let index = 0; index < payload.edits.length; index++) {
|
|
328
|
-
const verdict = validateDesignRequest({ ...payload.edits[index], previewId, revision, cid }, registry, room)
|
|
329
|
-
if (!verdict.ok) return { ...verdict, editIndex: index }
|
|
330
|
-
const targetKey = verdict.request.target.sourceId || verdict.request.target.nodeKey || verdict.request.target.selector || verdict.request.target.xpath
|
|
331
|
-
if (targets.has(targetKey)) return { ok: false, error: 'duplicate-target', editIndex: index }
|
|
332
|
-
targets.add(targetKey)
|
|
333
|
-
record = verdict.record
|
|
334
|
-
edits.push(verdict.request)
|
|
335
|
-
}
|
|
336
|
-
return { ok: true, record, request: { previewId, revision: record.revision, mode: 'batch', edits, cid } }
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export async function deleteDesignAssetDraft({ webBase, token, room, path: assetPath, fetcher = fetch }) {
|
|
340
|
-
if (!webBase || !token || !room || !assetPath) return false
|
|
341
|
-
const response = await fetcher(`${String(webBase).replace(/\/$/, '')}/api/code-design-draft`, {
|
|
342
|
-
method: 'DELETE',
|
|
343
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
344
|
-
body: JSON.stringify({ code: room, path: assetPath }),
|
|
345
|
-
}).catch(() => null)
|
|
346
|
-
return !!response?.ok
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export async function materializeDesignAsset({ webBase, token, room, asset, requestId, cleanup = true, fetcher = fetch }) {
|
|
350
|
-
if (!webBase || !token || !room || !asset?.path || !requestId) return null
|
|
351
|
-
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }
|
|
352
|
-
const signed = await fetcher(`${String(webBase).replace(/\/$/, '')}/api/code-attachment-url`, {
|
|
353
|
-
method: 'POST',
|
|
354
|
-
headers,
|
|
355
|
-
body: JSON.stringify({ code: room, path: asset.path }),
|
|
356
|
-
})
|
|
357
|
-
if (!signed.ok) return null
|
|
358
|
-
const { url } = await signed.json()
|
|
359
|
-
if (!url) return null
|
|
360
|
-
const response = await fetcher(url)
|
|
361
|
-
if (!response.ok) return null
|
|
362
|
-
const data = Buffer.from(await response.arrayBuffer())
|
|
363
|
-
if (!data.length || data.length > 10 * 1024 * 1024) return null
|
|
364
|
-
const ext = path.extname(asset.name).replace(/[^.a-zA-Z0-9]/g, '').slice(0, 9) || '.img'
|
|
365
|
-
const dir = path.join(os.tmpdir(), 'thinkpool-design-assets')
|
|
366
|
-
const file = path.join(dir, `${clean(requestId, 80)}${ext}`)
|
|
367
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
368
|
-
fs.writeFileSync(file, data)
|
|
369
|
-
if (cleanup) await deleteDesignAssetDraft({ webBase, token, room, path: asset.path, fetcher })
|
|
370
|
-
return file
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function designAction(request) {
|
|
374
|
-
if (request.mode === 'text') return `Replace only this element's visible text with:\n${request.intent}`
|
|
375
|
-
if (request.mode === 'move') return `Move only this selected element within its current direct parent, from sibling position ${request.movement.fromIndex + 1} to ${request.movement.toIndex + 1}. Preserve the parent, responsive behavior, semantics, and every other sibling.`
|
|
376
|
-
if (request.mode === 'image') return `Replace only this selected image's content with the uploaded image at this bridge-local temporary path:\n${request.asset.localPath}\nCopy it into an appropriate durable project asset location. Preserve the existing image frame, dimensions, border radius, object-fit behavior, responsive rules, and surrounding layout.`
|
|
377
|
-
return `Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${request.intent}`
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function selectedElementBlock(request) {
|
|
381
|
-
const identity = request.sourceIdentity
|
|
382
|
-
? `\n\nBridge-verified application source:\n${JSON.stringify(request.sourceIdentity, null, 2)}`
|
|
383
|
-
: '\n\nApplication source identity: unavailable — selector/XPath fallback only.'
|
|
384
|
-
return `Selected element:\n${JSON.stringify(request.target, null, 2)}${identity}`
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const designModeLabel = (mode) => ({ text: 'Text', prompt: 'Prompt', move: 'Move', image: 'Image' })[mode] || 'Edit'
|
|
388
|
-
|
|
389
|
-
function visibleDesignAction(request) {
|
|
390
|
-
if (request.mode === 'text') return `Replacement text:\n${clean(request.intent, MAX_TEXT)}`
|
|
391
|
-
if (request.mode === 'prompt') return `Instruction:\n${clean(request.intent, MAX_INTENT)}`
|
|
392
|
-
if (request.mode === 'move') return `Move from position ${request.movement.fromIndex + 1} to position ${request.movement.toIndex + 1}.`
|
|
393
|
-
if (request.mode === 'image') return `Replacement image: ${clean(request.asset?.name || 'uploaded image', 240)}`
|
|
394
|
-
return ''
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Complete, room-safe transcript text for the visible Design dispatch. Keep the
|
|
398
|
-
// user instructions and order; omit host-only paths and locator packets.
|
|
399
|
-
export function designTranscript(request, { restore = false } = {}) {
|
|
400
|
-
if (restore) return 'Thinkpool Design · restore the previous verified revision'
|
|
401
|
-
const edits = request?.mode === 'batch' ? request.edits : [request]
|
|
402
|
-
const valid = Array.isArray(edits) ? edits.filter(Boolean).slice(0, MAX_BATCH_EDITS) : []
|
|
403
|
-
const heading = request?.mode === 'batch'
|
|
404
|
-
? `Thinkpool Design · ${valid.length} pooled edit${valid.length === 1 ? '' : 's'}`
|
|
405
|
-
: 'Thinkpool Design · 1 edit'
|
|
406
|
-
const details = valid.map((edit, index) => {
|
|
407
|
-
const target = clean(edit.target?.name || edit.target?.tag || 'element', 240)
|
|
408
|
-
return `${index + 1}. ${designModeLabel(edit.mode)} · ${target}\n${visibleDesignAction(edit)}`
|
|
409
|
-
})
|
|
410
|
-
return [heading, ...details].join('\n\n')
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function designPrompt({ record, request, by, restore = false, priorRecord = null }) {
|
|
414
|
-
const batch = request.mode === 'batch' ? request.edits : null
|
|
415
|
-
const work = restore
|
|
416
|
-
? `Restore only the immediately previous Design Mode change by comparing the current artifact with this bridge-created verified backup: ${priorRecord?.backupPath || '(backup unavailable)'}. Do not overwrite unrelated changes.`
|
|
417
|
-
: batch
|
|
418
|
-
? batch.map((edit, index) => `Edit ${index + 1} of ${batch.length} · ${edit.mode}\n${selectedElementBlock(edit)}\n\n${designAction(edit)}`).join('\n\n---\n\n')
|
|
419
|
-
: `${selectedElementBlock(request)}\n\n${designAction(request)}`
|
|
420
|
-
if (record.sourceKind === 'preview') {
|
|
421
|
-
return `A room member used ThinkPool Design Mode on a rendered application preview. This is an authorized source edit in your current workspace.\n\nRendered snapshot (read-only evidence): ${record.sourcePath}\nApplication workspace: ${record.workspaceRoot}\nPreview root: ${record.previewRoot}\nPreview route: ${record.route}\nExpected snapshot revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${batch ? `${batch.length} queued edits in one batch` : request.mode}\n\n${work}\n\nRules:\n- Re-read the rendered snapshot and confirm the selected element still matches before editing.\n- Do not edit the snapshot. For an edit with bridge-verified application source, start at that exact relative file and range. For a selector/XPath fallback, locate the represented element in application source and do not call the match exact.\n- Before editing a bridge-verified target, confirm the current source still hashes to the supplied fileHash. If it does not, stop and request a fresh capture/reselection.\n- If fallback selector, text, or surrounding structure does not identify one application source location unambiguously, stop without editing and explain that the person must reselect it.\n${batch ? '- Apply the queued edits in order in one source pass, then build and capture once after the full batch.' : '- Apply only the requested change, then build and capture once.'}\n- Preserve the product page faithfully: keep its real copy, structure, fonts, assets, spacing, responsive behavior, and unrelated source unchanged.\n- Run the project build, then use preview_start with root ${record.previewRoot} and preview_capture with path ${record.route}, both viewports, and title ${JSON.stringify(record.title || record.slug || 'Design preview')}. The bridge correlates that capture to this request.\n- Do not claim the edit is verified until the room receives the new desktop and mobile capture.\n- If the build or capture fails, report the failure instead of substituting a screenshot-only result.`
|
|
422
|
-
}
|
|
423
|
-
return `A room member used ThinkPool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${record.sourcePath}\nWorkspace: ${record.workspaceRoot}\nExpected source revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${restore ? 'restore the immediately previous verified Design Mode revision' : batch ? `${batch.length} queued edits in one batch` : request.mode}\n\n${work}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches every selected element before editing.\n${batch ? '- Apply the queued edits in order in one source pass, then render once after the full batch.' : '- Apply only the requested change, then render the result once.'}\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve the represented product page faithfully: keep its real copy, structure, fonts, assets, spacing, and responsive behavior except for the selected edits. Never replace it with generic mockup content.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${request.cid} and TP_DESIGN_PARENT_REVISION=${record.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If any target is stale or ambiguous, stop without editing and explain that the person must reselect it.`
|
|
424
|
-
}
|
|
1
|
+
import e from"node:crypto";import t from"node:fs";import r from"node:os";import n from"node:path";import{DESIGN_SOURCE_MAP_RELATIVE as o,DESIGN_SOURCE_MAP_VERSION as i,MAX_DESIGN_SOURCE_ENTRIES as s,MAX_DESIGN_SOURCE_MAP_BYTES as a}from"./design-source-contract.mjs";const c=2097152,u=4e3,l=new Set(["dist","build",".next",".git","coverage","node_modules"]),d=(e,t)=>String(e??"").replace(/\0/g,"").trim().slice(0,t),p=(e,t)=>{const r=n.relative(e,t);return""===r||!r.startsWith("..")&&!n.isAbsolute(r)};export async function syncRealtimeAuth(e,t,r=null){const n=t||r;if(!n||"function"!=typeof e?.realtime?.setAuth)return!1;try{return await e.realtime.setAuth(n),!0}catch{return!1}}export function sourceRevision(t){return e.createHash("sha256").update(String(t)).digest("hex").slice(0,24)}export function resolveDesignSourceMap(e,r,u){if(!e||!r||!u)return null;let h,m,f,g,v,y;try{h=t.realpathSync(e),f=t.realpathSync(r),m=t.realpathSync(n.join(f,o)),g=t.realpathSync(u);const i=t.statSync(h);if(!i.isFile()||i.size<=0||i.size>a)return null;if(h!==m||!p(g,f)||!p(f,h))return null;v=t.readFileSync(h,"utf8"),y=JSON.parse(v)}catch{return null}if(y?.version!==i||!y.entries||"object"!=typeof y.entries||Array.isArray(y.entries))return null;const b=Object.entries(y.entries);if(b.length>s)return null;const w=new Map,x=new Map;for(const[e,r]of b){if(!/^[A-Za-z0-9_-]{8,80}$/.test(e)||!r||"object"!=typeof r)return null;const o=d(r.file,700);if(!o||n.isAbsolute(o)||o.split(/[\\/]/).some(e=>".."===e||l.has(e)))return null;let i=w.get(o);if(!i){try{i=t.realpathSync(n.resolve(g,o));const e=t.statSync(i);if(!e.isFile()||e.size>c||!p(g,i))return null}catch{return null}w.set(o,i)}const s=Number(r.start),a=Number(r.end),u=Number(r.line),h=Number(r.column),m=d(r.tag,40).toLowerCase(),f=d(r.fileHash,64).toLowerCase();if(![s,a,u,h].every(Number.isSafeInteger)||s<0||a<=s||u<1||h<0)return null;if(!/^[a-z][a-z0-9:-]*$/.test(m)||!/^[a-f0-9]{24}$/.test(f))return null;x.set(e,{sourceId:e,file:o.split(n.sep).join("/"),sourcePath:i,start:s,end:a,line:u,column:h,tag:m,fileHash:f})}return{path:h,revision:sourceRevision(v),buildId:d(y.buildId,80),entries:x}}export function resolveDesignSource(r,o){if(!r||!o)return null;let i,s;try{i=t.realpathSync(r),s=t.realpathSync(o);const e=t.statSync(i);if(!e.isFile()||e.size>c)return null}catch{return null}const a=n.relative(s,i);if(!a||a.startsWith("..")||n.isAbsolute(a)||".html"!==n.extname(i).toLowerCase())return null;if(a.split(n.sep).some(e=>["dist","build",".next",".git","coverage","node_modules"].includes(e)))return null;const u=t.readFileSync(i,"utf8"),l=sourceRevision(u);return{previewId:e.createHash("sha256").update(`${i}\0${l}`).digest("base64url").slice(0,32),revision:l,sourcePath:i,workspaceRoot:s,source:u}}export function resolvePreviewDesignSource(r,o,i){if(!(r&&"preview"===r.sourceKind&&r.snapshot&&o&&i))return null;let s,a,u;try{s=t.realpathSync(r.snapshot),a=t.realpathSync(o),u=t.realpathSync(i);const e=t.statSync(s);if(!e.isFile()||e.size>c)return null}catch{return null}const l=n.relative(u,s);if(!l||l.startsWith("..")||n.isAbsolute(l)||".html"!==n.extname(s).toLowerCase())return null;const h=d(r.route||"/",500),m=d(r.previewRoot||"dist",500),f=d(r.captureKey,1e3);if(!h.startsWith("/")||!f||f!==JSON.stringify([m,h]))return null;const g=t.readFileSync(s,"utf8");let v=null;try{if(v=t.realpathSync(n.resolve(a,m)),!p(a,v))return null}catch{return null}const y=r.sourceMap?resolveDesignSourceMap(r.sourceMap,v,a):null,b=sourceRevision(y?`${g}\0${y.revision}`:g);return{sourceKind:"preview",previewId:e.createHash("sha256").update(`${a}\0${f}\0${b}`).digest("base64url").slice(0,32),revision:b,sourcePath:s,workspaceRoot:a,source:g,outbox:u,route:h,previewRoot:m,captureKey:f,sourceMap:y,sourceMapPath:y?.path||null,sourceMapDeclared:!!r.sourceMap}}export function resolveManifestDesignSource(e,t,{box:r}={}){return e&&"object"==typeof e?"preview"===e.sourceKind?resolvePreviewDesignSource(e,t,r):resolveDesignSource(e.source||e.html,t):null}export function resolveManifestDisplaySource(e,r,{box:o}={}){if(!r)return null;if("preview"===r.sourceKind||!e?.snapshot)return r.source;let i,s;try{i=t.realpathSync(e.snapshot),s=t.realpathSync(o);const r=t.statSync(i);if(!r.isFile()||r.size>c)return null}catch{return null}const a=n.relative(s,i);return!a||a.startsWith("..")||n.isAbsolute(a)||".html"!==n.extname(i).toLowerCase()?null:t.readFileSync(i,"utf8")}export function refreshDesignSource(e){return e?"preview"!==e.sourceKind?resolveDesignSource(e.sourcePath,e.workspaceRoot):resolvePreviewDesignSource({sourceKind:"preview",snapshot:e.sourcePath,route:e.route,previewRoot:e.previewRoot,captureKey:e.captureKey,sourceMap:e.sourceMapPath},e.workspaceRoot,e.outbox):null}export function restoreDesignSources(e,r,o,i=100){if(!e||!r||!o)return[];let s;try{s=t.readdirSync(e,{withFileTypes:!0}).filter(e=>e.isFile()&&e.name.endsWith(".json")&&!e.name.endsWith(".receipt.json")).map(r=>{const o=n.join(e,r.name);return{file:o,mtimeMs:t.statSync(o).mtimeMs}}).sort((e,t)=>t.mtimeMs-e.mtimeMs).slice(0,Math.max(0,i))}catch{return[]}const a=[];for(const{file:i}of s)try{const s=JSON.parse(t.readFileSync(i,"utf8")),c=resolveManifestDesignSource(s,r,{box:e});if(!c)continue;Object.assign(c,{term:o,slug:String(s.slug||""),title:String(s.title||s.slug||""),desktop:s.desktop||null,mobile:s.mobile||null});const u=n.join(e,".design-revisions",`${c.revision}.html`);t.existsSync(u)&&(c.backupPath=u),a.push(c)}catch{}return a}export function resolveRequestSourceIdentity(e,r){if(!r?.sourceId)return{ok:!0,identity:null};const n=e?.sourceMap?.entries?.get(r.sourceId);if(!n)return{ok:!1,error:e?.sourceMapDeclared?"stale-source-identity":"source-identity-unavailable"};if(r.tag&&n.tag!==r.tag)return{ok:!1,error:"source-identity-mismatch"};let o;try{o=t.readFileSync(n.sourcePath,"utf8")}catch{return{ok:!1,error:"stale-source-identity"}}return sourceRevision(o)!==n.fileHash?{ok:!1,error:"stale-source-identity"}:{ok:!0,identity:{sourceId:n.sourceId,file:n.file,start:n.start,end:n.end,line:n.line,column:n.column,tag:n.tag,fileHash:n.fileHash}}}export function validateDesignRequest(e,t,r=""){if(!e||"object"!=typeof e)return{ok:!1,error:"invalid-request"};const n=d(e.previewId,64),o=t.get(n);if(!o)return{ok:!1,error:"preview-unavailable"};if(d(e.revision,64)!==o.revision)return{ok:!1,error:"stale-preview"};const i=function(e){if(!e||"object"!=typeof e)return null;const t=e.rect&&"object"==typeof e.rect?{x:Number(e.rect.x)||0,y:Number(e.rect.y)||0,width:Math.max(0,Number(e.rect.width)||0),height:Math.max(0,Number(e.rect.height)||0)}:null;return{selector:d(e.selector,600),xpath:d(e.xpath,600),tag:d(e.tag,40).toLowerCase(),role:d(e.role,80),name:d(e.name,240),text:d(e.text,u),canEditText:!0===e.canEditText,canReplaceImage:!0===e.canReplaceImage,nodeKey:d(e.nodeKey,80),parentNodeKey:d(e.parentNodeKey,80),sourceId:d(e.sourceId,80),parentSourceId:d(e.parentSourceId,80),rect:t}}(e.target);if(!i?.selector&&!i?.xpath)return{ok:!1,error:"select-an-element"};const s=["text","prompt","move","image"].includes(e.mode)?e.mode:null;if(!s)return{ok:!1,error:"invalid-mode"};const a=d(e.intent,"text"===s?u:2e3);if(["text","prompt"].includes(s)&&!a)return{ok:!1,error:"text"===s?"enter-text":"describe-change"};if("text"===s&&!i.canEditText)return{ok:!1,error:"text-edit-not-safe"};const c="move"===s?function(e){if(!e||"object"!=typeof e)return null;const t=Number(e.fromIndex),r=Number(e.toIndex);return![t,r].every(Number.isSafeInteger)||t<0||r<0||t>499||r>499?null:{fromIndex:t,toIndex:r}}(e.movement):null;if("move"===s&&(!c||c.fromIndex===c.toIndex))return{ok:!1,error:"invalid-move"};const l="image"===s?function(e,t){if(!e||"object"!=typeof e)return null;const r={path:d(e.path,700),name:d(e.name,240),mediaType:d(e.mediaType,120).toLowerCase(),size:Number(e.size)},n=t?`code/${String(t).toUpperCase()}/design-drafts/`:"code/";return r.path.startsWith(n)&&!r.path.split("/").includes("..")&&r.mediaType.startsWith("image/")?!Number.isSafeInteger(r.size)||r.size<=0||r.size>10485760?null:r:null}(e.asset,r):null;if(!("image"!==s||i.canReplaceImage&&l))return{ok:!1,error:"invalid-image"};const p=resolveRequestSourceIdentity(o,i);return p.ok?{ok:!0,record:o,request:{previewId:n,revision:o.revision,mode:s,intent:a,target:i,movement:c,asset:l,sourceIdentity:p.identity,cid:d(e.cid,80)}}:p}export function validateDesignBatchRequest(e,t,r=""){if(!e||"object"!=typeof e||!Array.isArray(e.edits))return validateDesignRequest(e,t,r);if(e.edits.length<1||e.edits.length>8)return{ok:!1,error:"invalid-batch"};const n=d(e.previewId,64),o=d(e.revision,64),i=d(e.cid,80),s=[],a=new Set;let c=null;for(let u=0;u<e.edits.length;u++){const l=validateDesignRequest({...e.edits[u],previewId:n,revision:o,cid:i},t,r);if(!l.ok)return{...l,editIndex:u};const d=l.request.target.sourceId||l.request.target.nodeKey||l.request.target.selector||l.request.target.xpath;if(a.has(d))return{ok:!1,error:"duplicate-target",editIndex:u};a.add(d),c=l.record,s.push(l.request)}return{ok:!0,record:c,request:{previewId:n,revision:c.revision,mode:"batch",edits:s,cid:i}}}export async function deleteDesignAssetDraft({webBase:e,token:t,room:r,path:n,fetcher:o=fetch}){if(!(e&&t&&r&&n))return!1;const i=await o(`${String(e).replace(/\/$/,"")}/api/code-design-draft`,{method:"DELETE",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({code:r,path:n})}).catch(()=>null);return!!i?.ok}export async function materializeDesignAsset({webBase:e,token:o,room:i,asset:s,requestId:a,cleanup:c=!0,fetcher:u=fetch}){if(!(e&&o&&i&&s?.path&&a))return null;const l={"Content-Type":"application/json",Authorization:`Bearer ${o}`},p=await u(`${String(e).replace(/\/$/,"")}/api/code-attachment-url`,{method:"POST",headers:l,body:JSON.stringify({code:i,path:s.path})});if(!p.ok)return null;const{url:h}=await p.json();if(!h)return null;const m=await u(h);if(!m.ok)return null;const f=Buffer.from(await m.arrayBuffer());if(!f.length||f.length>10485760)return null;const g=n.extname(s.name).replace(/[^.a-zA-Z0-9]/g,"").slice(0,9)||".img",v=n.join(r.tmpdir(),"thinkpool-design-assets"),y=n.join(v,`${d(a,80)}${g}`);return t.mkdirSync(v,{recursive:!0}),t.writeFileSync(y,f),c&&await deleteDesignAssetDraft({webBase:e,token:o,room:i,path:s.path,fetcher:u}),y}function h(e){return"text"===e.mode?`Replace only this element's visible text with:\n${e.intent}`:"move"===e.mode?`Move only this selected element within its current direct parent, from sibling position ${e.movement.fromIndex+1} to ${e.movement.toIndex+1}. Preserve the parent, responsive behavior, semantics, and every other sibling.`:"image"===e.mode?`Replace only this selected image's content with the uploaded image at this bridge-local temporary path:\n${e.asset.localPath}\nCopy it into an appropriate durable project asset location. Preserve the existing image frame, dimensions, border radius, object-fit behavior, responsive rules, and surrounding layout.`:`Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${e.intent}`}function m(e){const t=e.sourceIdentity?`\n\nBridge-verified application source:\n${JSON.stringify(e.sourceIdentity,null,2)}`:"\n\nApplication source identity: unavailable — selector/XPath fallback only.";return`Selected element:\n${JSON.stringify(e.target,null,2)}${t}`}export function designTranscript(e,{restore:t=!1}={}){if(t)return"Thinkpool Design · restore the previous verified revision";const r="batch"===e?.mode?e.edits:[e],n=Array.isArray(r)?r.filter(Boolean).slice(0,8):[],o="batch"===e?.mode?`Thinkpool Design · ${n.length} pooled edit${1===n.length?"":"s"}`:"Thinkpool Design · 1 edit",i=n.map((e,t)=>{const r=d(e.target?.name||e.target?.tag||"element",240);return`${t+1}. ${n=e.mode,{text:"Text",prompt:"Prompt",move:"Move",image:"Image"}[n]||"Edit"} · ${r}\n${function(e){return"text"===e.mode?`Replacement text:\n${d(e.intent,u)}`:"prompt"===e.mode?`Instruction:\n${d(e.intent,2e3)}`:"move"===e.mode?`Move from position ${e.movement.fromIndex+1} to position ${e.movement.toIndex+1}.`:"image"===e.mode?`Replacement image: ${d(e.asset?.name||"uploaded image",240)}`:""}(e)}`;var n});return[o,...i].join("\n\n")}export function designPrompt({record:e,request:t,by:r,restore:n=!1,priorRecord:o=null}){const i="batch"===t.mode?t.edits:null,s=n?`Restore only the immediately previous Design Mode change by comparing the current artifact with this bridge-created verified backup: ${o?.backupPath||"(backup unavailable)"}. Do not overwrite unrelated changes.`:i?i.map((e,t)=>`Edit ${t+1} of ${i.length} · ${e.mode}\n${m(e)}\n\n${h(e)}`).join("\n\n---\n\n"):`${m(t)}\n\n${h(t)}`;return"preview"===e.sourceKind?`A room member used ThinkPool Design Mode on a rendered application preview. This is an authorized source edit in your current workspace.\n\nRendered snapshot (read-only evidence): ${e.sourcePath}\nApplication workspace: ${e.workspaceRoot}\nPreview root: ${e.previewRoot}\nPreview route: ${e.route}\nExpected snapshot revision: ${e.revision}\nRequested by: ${d(r||"A room member",120)}\nOperation: ${i?`${i.length} queued edits in one batch`:t.mode}\n\n${s}\n\nRules:\n- Re-read the rendered snapshot and confirm the selected element still matches before editing.\n- Do not edit the snapshot. For an edit with bridge-verified application source, start at that exact relative file and range. For a selector/XPath fallback, locate the represented element in application source and do not call the match exact.\n- Before editing a bridge-verified target, confirm the current source still hashes to the supplied fileHash. If it does not, stop and request a fresh capture/reselection.\n- If fallback selector, text, or surrounding structure does not identify one application source location unambiguously, stop without editing and explain that the person must reselect it.\n${i?"- Apply the queued edits in order in one source pass, then build and capture once after the full batch.":"- Apply only the requested change, then build and capture once."}\n- Preserve the product page faithfully: keep its real copy, structure, fonts, assets, spacing, responsive behavior, and unrelated source unchanged.\n- Run the project build, then use preview_start with root ${e.previewRoot} and preview_capture with path ${e.route}, both viewports, and title ${JSON.stringify(e.title||e.slug||"Design preview")}. The bridge correlates that capture to this request.\n- Do not claim the edit is verified until the room receives the new desktop and mobile capture.\n- If the build or capture fails, report the failure instead of substituting a screenshot-only result.`:`A room member used ThinkPool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${e.sourcePath}\nWorkspace: ${e.workspaceRoot}\nExpected source revision: ${e.revision}\nRequested by: ${d(r||"A room member",120)}\nOperation: ${n?"restore the immediately previous verified Design Mode revision":i?`${i.length} queued edits in one batch`:t.mode}\n\n${s}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches every selected element before editing.\n${i?"- Apply the queued edits in order in one source pass, then render once after the full batch.":"- Apply only the requested change, then render the result once."}\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve the represented product page faithfully: keep its real copy, structure, fonts, assets, spacing, and responsive behavior except for the selected edits. Never replace it with generic mockup content.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${t.cid} and TP_DESIGN_PARENT_REVISION=${e.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If any target is stale or ambiguous, stop without editing and explain that the person must reselect it.`}
|
|
@@ -1,4 +1 @@
|
|
|
1
|
-
export const DESIGN_SOURCE_MAP_VERSION =
|
|
2
|
-
export const DESIGN_SOURCE_MAP_RELATIVE = '.thinkpool-design/source-map.json'
|
|
3
|
-
export const MAX_DESIGN_SOURCE_MAP_BYTES = 2 * 1024 * 1024
|
|
4
|
-
export const MAX_DESIGN_SOURCE_ENTRIES = 50_000
|
|
1
|
+
export const DESIGN_SOURCE_MAP_VERSION=1;export const DESIGN_SOURCE_MAP_RELATIVE=".thinkpool-design/source-map.json";export const MAX_DESIGN_SOURCE_MAP_BYTES=2097152;export const MAX_DESIGN_SOURCE_ENTRIES=5e4;
|
package/direct-pair-room.mjs
CHANGED
|
@@ -1,57 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
// A standalone room bridge is already the authoritative process for its room. It
|
|
4
|
-
// must be reachable over the pair bus even when the account-level supervisor is
|
|
5
|
-
// not running. Keep the identity and request handling in a small tested module so
|
|
6
|
-
// the standalone path preserves the same pair-only and bounded-post invariants as
|
|
7
|
-
// account.mjs.
|
|
8
|
-
|
|
9
|
-
export function standalonePairIdentity ({ row, myUid } = {}) {
|
|
10
|
-
const participants = Array.isArray(row?.participants) ? row.participants.map(String) : []
|
|
11
|
-
const me = String(myUid || '')
|
|
12
|
-
if (!me || String(row?.owner_id || '') !== me || participants.length !== 2 || !participants.includes(me)) return null
|
|
13
|
-
const partnerUid = participants.find((uid) => uid !== me)
|
|
14
|
-
const key = pairKeyFor(me, partnerUid)
|
|
15
|
-
if (!partnerUid || !key) return null
|
|
16
|
-
return { partnerUid, key, topic: pairTopic(key) }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createStandalonePairResponder ({
|
|
20
|
-
room,
|
|
21
|
-
roomName = null,
|
|
22
|
-
host = null,
|
|
23
|
-
formatPeek,
|
|
24
|
-
receivePost,
|
|
25
|
-
now = () => Date.now(),
|
|
26
|
-
postLimit = 6,
|
|
27
|
-
postWindowMs = 60_000,
|
|
28
|
-
} = {}) {
|
|
29
|
-
if (!room || typeof formatPeek !== 'function' || typeof receivePost !== 'function') {
|
|
30
|
-
throw new TypeError('room, formatPeek, and receivePost are required')
|
|
31
|
-
}
|
|
32
|
-
let bucket = { count: 0, resetAt: 0 }
|
|
33
|
-
|
|
34
|
-
const isTarget = (payload) => String(payload?.targetRoom || '').toUpperCase().trim() === room
|
|
35
|
-
const allowPost = () => {
|
|
36
|
-
const t = now()
|
|
37
|
-
if (t >= bucket.resetAt) bucket = { count: 0, resetAt: t + postWindowMs }
|
|
38
|
-
if (bucket.count >= postLimit) return false
|
|
39
|
-
bucket.count += 1
|
|
40
|
-
return true
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return Object.freeze({
|
|
44
|
-
list: (payload) => ({ nonce: payload?.nonce, rooms: [{ code: room, name: roomName, host }] }),
|
|
45
|
-
peek: (payload) => {
|
|
46
|
-
if (!isTarget(payload)) return null
|
|
47
|
-
try { return { nonce: payload?.nonce, text: formatPeek(payload) } }
|
|
48
|
-
catch { return { nonce: payload?.nonce, error: 'Could not read that session.' } }
|
|
49
|
-
},
|
|
50
|
-
post: async (payload) => {
|
|
51
|
-
if (!isTarget(payload)) return null
|
|
52
|
-
if (!allowPost()) return { nonce: payload?.nonce, error: 'Too many cross-room posts to this device right now — try again shortly.' }
|
|
53
|
-
try { return { nonce: payload?.nonce, ...(await receivePost(payload)) } }
|
|
54
|
-
catch { return { nonce: payload?.nonce, error: `Could not deliver to room ${room}.` } }
|
|
55
|
-
},
|
|
56
|
-
})
|
|
57
|
-
}
|
|
1
|
+
import{pairKeyFor as n,pairTopic as t}from"./cross-terminal.mjs";export function standalonePairIdentity({row:r,myUid:e}={}){const o=Array.isArray(r?.participants)?r.participants.map(String):[],i=String(e||"");if(!i||String(r?.owner_id||"")!==i||2!==o.length||!o.includes(i))return null;const c=o.find(n=>n!==i),s=n(i,c);return c&&s?{partnerUid:c,key:s,topic:t(s)}:null}export function createStandalonePairResponder({room:n,roomName:t=null,host:r=null,formatPeek:e,receivePost:o,now:i=()=>Date.now(),postLimit:c=6,postWindowMs:s=6e4}={}){if(!n||"function"!=typeof e||"function"!=typeof o)throw new TypeError("room, formatPeek, and receivePost are required");let a={count:0,resetAt:0};const u=t=>String(t?.targetRoom||"").toUpperCase().trim()===n;return Object.freeze({list:e=>({nonce:e?.nonce,rooms:[{code:n,name:t,host:r}]}),peek:n=>{if(!u(n))return null;try{return{nonce:n?.nonce,text:e(n)}}catch{return{nonce:n?.nonce,error:"Could not read that session."}}},post:async t=>{if(!u(t))return null;if(!(()=>{const n=i();return n>=a.resetAt&&(a={count:0,resetAt:n+s}),!(a.count>=c||(a.count+=1,0))})())return{nonce:t?.nonce,error:"Too many cross-room posts to this device right now — try again shortly."};try{return{nonce:t?.nonce,...await o(t)}}catch{return{nonce:t?.nonce,error:`Could not deliver to room ${n}.`}}}})}
|
package/dispatch-lease.mjs
CHANGED
|
@@ -1,37 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Durable permission rows can outlive the agent turn that requested them. The
|
|
4
|
-
// lease is the bridge-local execution fence: arguments still need the durable
|
|
5
|
-
// fingerprint/authority checks, and the matching intent must also remain current
|
|
6
|
-
// until the lane is created. A newer human turn or abort supersedes the lease, so
|
|
7
|
-
// a late (otherwise valid) approval cannot resurrect an old runtime choice.
|
|
8
|
-
|
|
9
|
-
const nextGeneration = (entry) =>
|
|
10
|
-
(Number.isSafeInteger(entry?.dispatchGeneration) ? entry.dispatchGeneration : 0) + 1
|
|
11
|
-
|
|
12
|
-
export function beginDispatchLease (entry, permissionId) {
|
|
13
|
-
if (!entry || !permissionId) return Object.freeze({ ok: false, code: 'invalid_dispatch' })
|
|
14
|
-
if (entry.dispatchLease) return Object.freeze({ ok: false, code: 'dispatch_pending', active: entry.dispatchLease })
|
|
15
|
-
const lease = Object.freeze({ permissionId, generation: nextGeneration(entry) })
|
|
16
|
-
entry.dispatchGeneration = lease.generation
|
|
17
|
-
entry.dispatchLease = lease
|
|
18
|
-
return Object.freeze({ ok: true, lease })
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isDispatchLeaseCurrent (entry, lease) {
|
|
22
|
-
return Boolean(entry && lease && entry.dispatchLease === lease && entry.dispatchGeneration === lease.generation)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function finishDispatchLease (entry, lease) {
|
|
26
|
-
if (!isDispatchLeaseCurrent(entry, lease)) return false
|
|
27
|
-
entry.dispatchLease = null
|
|
28
|
-
return true
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function supersedeDispatchLease (entry) {
|
|
32
|
-
const lease = entry?.dispatchLease || null
|
|
33
|
-
if (!entry || !lease) return null
|
|
34
|
-
entry.dispatchGeneration = nextGeneration(entry)
|
|
35
|
-
entry.dispatchLease = null
|
|
36
|
-
return lease
|
|
37
|
-
}
|
|
1
|
+
const e=e=>(Number.isSafeInteger(e?.dispatchGeneration)?e.dispatchGeneration:0)+1;export function beginDispatchLease(t,n){if(!t||!n)return Object.freeze({ok:!1,code:"invalid_dispatch"});if(t.dispatchLease)return Object.freeze({ok:!1,code:"dispatch_pending",active:t.dispatchLease});const i=Object.freeze({permissionId:n,generation:e(t)});return t.dispatchGeneration=i.generation,t.dispatchLease=i,Object.freeze({ok:!0,lease:i})}export function isDispatchLeaseCurrent(e,t){return Boolean(e&&t&&e.dispatchLease===t&&e.dispatchGeneration===t.generation)}export function finishDispatchLease(e,t){return!!isDispatchLeaseCurrent(e,t)&&(e.dispatchLease=null,!0)}export function supersedeDispatchLease(t){const n=t?.dispatchLease||null;return t&&n?(t.dispatchGeneration=e(t),t.dispatchLease=null,n):null}
|