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.
Files changed (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,53 +1 @@
1
- // Cumulative provider snapshots are replace-in-place UI state, not transcript
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 = 1
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;
@@ -1,57 +1 @@
1
- import { pairKeyFor, pairTopic } from './cross-terminal.mjs'
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}.`}}}})}
@@ -1,37 +1 @@
1
- // One live dispatch intent per structured terminal.
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}